/* PPT Master - Strategist confirmation UI * Stage 1 confirms the communication contract and the free-design/template * choice together. Stage 2 combines the coherent deck solution and production * mechanics, then writes the final result. * Finite fields use /static/catalogs.json; coordinated design directions seed * mode, visual style, color, typography, icons, and generated-image rendering, * while template-application prose stays conditional. Final confirm saves the * flattened current values to result.json. */ (function () { "use strict"; // ---- i18n ------------------------------------------------------------ var MESSAGES = { en: { page_title: "PPT Master - Confirm Design", topbar_hint: "Answer the open questions, or pick and customize recommendations, then continue.", stage_anchors: "Stage 1 · Communication contract", stage_final_plan: "Stage 2 · Final plan & production", loading: "Loading…", load_error: "Could not load the current recommendation stage. The AI must write it before launch.", btn_confirm: "Confirm", btn_confirm_contract: "Confirm contract & continue →", btn_confirm_final_plan: "Confirm final plan →", deriving: "Generating the downstream options from your choices…", template_selection_required: "Choose free design or use templates. When using templates, select at least one workspace.", connection_lost: "Connection to the confirm server was interrupted; retrying. If this keeps failing, return to the chat for confirmation.", confirmed_title: "✓ Confirmed", confirmed_hint: "Your choices are saved. You can close this page and return to the chat.", lang_toggle_title: "Switch language", sec_template_choice: "Design basis", template_choice_hint: "Choose how this deck should establish its design system.", template_free_title: "Design from the current content", template_free_desc: "Use no reusable template workspace. The Strategist will derive the visual system from this project.", template_use_title: "Use templates", template_use_desc: "Select one or more reusable Brand, Style, Layout, Deck, or specified workspaces.", sec_template_library: "Template combination", template_library_hint: "Choose at most one registered template for each type. Brand, Style, Layout, and Deck can be combined.", sec_template_explicit: "Specified templates", template_explicit_hint: "Choose at most one exact workspace supplied for this run. Its source path is shown for verification.", template_kind_brand: "Brand", template_kind_style: "Style", template_kind_layout: "Layout", template_kind_deck: "Deck", template_source_library: "Library", template_source_explicit: "Specified path", template_source_path: "Source path", template_select_none: "None", template_none_registered: "No registered templates", template_none_explicit: "No specified templates for this run", sec_canvas: "Canvas format", sec_pages: "Page count", sec_audience: "Target audience", sec_communication: "What this presentation must accomplish", sec_delivery: "How it will be used and what must remain", sec_narrative: "Narrative direction", sec_visual: "Visual direction", sec_color: "Color scheme", sec_icons: "Icon usage", sec_type: "Typography", sec_images: "Image usage", sec_image_production: "Image production", sec_proactive_execution: "Proactive execution", sec_mode: "Generation mode", sec_refine: "Review the Design Spec first", sec_design_directions: "Coherent design directions", design_directions_hint: "The recommended complete direction is applied first. Choose another or fine-tune the projected fields below; use Restore to return an adjusted direction to its authored bundle.", direction_active: "Applied", direction_adjusted: "Adjusted", direction_apply_hint: "Click to apply this complete direction.", direction_restore: "Restore authored direction", scheme_component_options: "Project-specific custom choices · select a card to edit", sec_template_application: "Template application", template_application_hint: "The AI recommends how to apply the installed template to this deck. Revise the plan directly in natural language.", placeholder_template_application: "Describe which template pages or prototypes to use, skip, repeat, or reorder; what must stay; and what may be replaced or reorganized.", sub_mode: "Narrative mode", sub_visual: "Visual style", sub_divergence: "Material divergence (how freely to reshape vs. stay close to the source)", placeholder_divergence: "In your words — e.g. \"stick closely to the document\" / \"freely restructure and expand within the source\". Leave blank for a balanced default.", communication_intent: "What should this presentation accomplish?", communication_intent_hint: "Open answer — combine any that apply: inform, explain, persuade, decide, align, teach, report/account, mobilize, or leave a record/hand-off. Describe priority or sequence when useful; do not choose labels.", placeholder_communication_intent: "e.g. Report progress and surface risk first, then secure a decision on the next investment.", audience_outcome: "Desired audience outcome / success condition", placeholder_audience_outcome: "What should the audience know, understand, believe, decide, or do afterward?", core_message: "Core message / decision ask / action", placeholder_core_message: "Which claims, requests, or actions must land even if little else is remembered?", delivery_context: "Delivery context (name the primary)", delivery_context_hint: "Distinguish presenter-led, reader-led, hybrid, or recorded/self-running. For hybrid use, state which mode leads and what secondary use must still work.", placeholder_delivery_context: "e.g. Primary: presenter-led 20-minute leadership review. Secondary: reader-led approval copy shared afterward.", artifact_afterlife: "Artifact afterlife", placeholder_artifact_afterlife: "e.g. approval, review, audit, archive, hand-off, or reuse; leave blank when no later use is expected.", stage1_current_value_hint: "Editable fields contain recommendations. Keep, revise, or clear them; confirmation saves the current text exactly, including blank values.", content_divergence_locked_hint: "This profile preserves the source wording and page structure, so this field is fixed.", custom: "Custom", custom_placeholder: "Type your own…", ai_custom_candidate: "AI custom proposal", ai_custom_candidate_hint: "Always visible for comparison. It is not selected by default; select it to edit.", custom_behavior_required: "The selected AI custom proposal cannot be blank.", custom_color_required: "Describe the custom color scheme before continuing.", design_system_required: "Choose a complete palette and typography system before continuing.", mode_behavior_placeholder: "Describe the act sequence, title voice, page rhythm, and presentation posture.", visual_style_behavior_placeholder: "Describe shape language, composition, decoration density, whitespace, typography character, and texture.", recommended: "Recommended", placeholder_audience: "Who is this deck for?", placeholder_pages: "e.g. 12-15", hex_override: "Custom HEX override:", formula_policy: "Formula rendering policy", image_ai_path: "AI image source", image_strategy: "Generated image style", image_strategy_empty: "No preset style references are available. You can still use a custom style.", image_strategy_required: "Choose a generated-image preset or describe a custom style.", image_strategy_invalid: "The selected generated-image preset is not available.", image_strategy_select_placeholder: "Choose a generated-image preset…", image_strategy_recommended_group: "Recommended for this deck", image_strategy_all_group: "All preset styles", image_strategy_rendering: "Rendering", image_strategy_visual: "Visual", image_strategy_mood: "Mood", image_strategy_ai_custom: "AI custom proposal", image_strategy_ai_custom_desc: "A novel or multi-reference rendering proposal. Select it to edit.", image_strategy_custom_placeholder: "Describe the exact generated-image direction, subjects, composition, style cues, or things to avoid.", image_strategy_reference_hint: "Reference images show rendering only. Final AI images inherit the deck color scheme selected above.", image_strategy_no_reference: "No reference image for this custom choice.", image_source_summary: "Selected image sources", image_production_hint: "Image sources and rendering are selected above. Resolve only the production path here.", image_usage_notes: "Additional image requirements", image_usage_notes_placeholder: "e.g. realistic handwashing scenes; avoid cartoon germs; keep product photos untouched.", image_usage_required: "Select at least one image usage option.", image_usage_none_exclusive: "No images cannot be combined with other image options.", proactive_execution_hint: "These defaults apply only when you have not explicitly instructed otherwise. Your latest explicit instruction always takes priority.", proactive_speaker_notes: "Proactively generate speaker notes", proactive_speaker_notes_desc: "On by default. The agent generates speaker notes without a separate request.", proactive_custom_animations: "Proactively create custom animations", proactive_custom_animations_desc: "Off by default. Strategist motion suggestions remain available; turn this on to have the agent create custom animations without a separate request.", proactive_narration_audio: "Proactively generate narration audio", proactive_narration_audio_desc: "Off by default. This raw choice does not rewrite the speaker-notes toggle; the Strategist resolves narration's effective notes dependency in the Design Spec.", font_heading: "Heading", font_body: "Body", font_selection: "Font selection", primary_language_font: "Primary-language font", english_font: "English font", font_picker_hint: "Choosing a recommendation fills these selectors. Changing any font marks the typography as customized.", other_installed_font: "Other installed font…", other_font_placeholder: "Exact installed font name", customized: "Customized", font_body_size: "Body baseline size", font_body_size_hint: "All type sizes derive from this body baseline.", body_size_unit_relation: "SVG px to PPT pt: 1px = 0.75pt.", body_size_pt_hint: "Approximately {pt} pt (1px = 0.75pt; saved as px).", role_size_pt_hint: "≈ {pt} pt", body_size_hint_canvas: "This canvas suggests ~{lo}–{hi}px (from its effective canvas span).", body_size_hint_purpose: "This reading mode recommends {def}px — one fixed size, not a range.", body_size_hint_oor: "(Current value is outside the usual range for this canvas — check the unit is right and that it fits.)", delivery_purpose: "Reading mode", delivery_purpose_hint: "Choose where the meaning lives: read-close decks explain themselves with complete sentences and detail; presenter-led decks use one idea, concise claims, and visual evidence.", size_override: "Per-role size override:", size_role_title: "title", size_role_subtitle: "subtitle", size_role_annotation: "annotation", custom_typography: "Custom typography", custom_color: "Custom color", custom_color_placeholder: "Describe your colors in words, e.g. deep navy primary, warm orange accent, white background — or paste HEX values…", role_background: "bg", role_secondary_bg: "2nd bg", role_primary: "primary", role_accent: "accent", role_secondary_accent: "2nd accent", role_body_text: "body text", cjk: "CJK", latin: "Latin", sample_heading_cjk: "主题方案标题", sample_heading_latin: "Presentation Title", sample_body_cjk: "关键信息摘要", sample_body_latin: "Key message summary", style_preview_label: "Overall impression (color + typography + icons)", style_preview_body: "· rough feel only, not the actual slide layout", no_icons: "No icons", preview_big_title: "Big Title", preview_section_title: "Section Title", preview_latin_title: "Section Title", preview_body_intro: "Body copy shows the baseline text rhythm and contrast.", preview_latin_body: "Body text sample for checking Latin typography.", preview_point_1_title: "Body content", preview_point_1_text: "Use this area to judge paragraph density and line spacing.", preview_point_2_title: "Key point", preview_point_2_text: "Icons are placed next to real text instead of floating alone.", preview_point_3_title: "Conclusion", preview_point_3_text: "The combination should stay readable at presentation scale.", mode_continuous_desc: "Generate the whole deck in one pass.", mode_split_desc: "Stop after the spec; resume SVG generation in a fresh window.", refine_off_desc: "Write the Design Spec and execution lock in sequence, then auto-proceed.", refine_on_desc: "Stop after the Design Spec. Revise any part in chat; approval then creates the execution lock and continues generation.", off_default: "Off", on: "On", option_prefix: "Option", error_retry: "Error - retry" }, ja: { page_title: "PPT Master - デザイン確認", topbar_hint: "自由記述の質問に答えるか、提案を選択・調整して次へ進んでください。", stage_anchors: "ステージ 1 · コミュニケーション契約", stage_final_plan: "ステージ 2 · 最終プランと制作", loading: "読み込み中…", load_error: "現在の推奨ステージを読み込めませんでした。起動前にAIが書き込む必要があります。", btn_confirm: "確定", btn_confirm_contract: "契約内容を確定して次へ →", btn_confirm_final_plan: "最終プランを確定 →", deriving: "選択内容をもとに後続の選択肢を生成しています…", template_selection_required: "自由デザインまたはテンプレート利用を選んでください。テンプレート利用時は、1つ以上のワークスペースを選択してください。", connection_lost: "確認ページのサーバー接続が中断されました。再試行しています。失敗が続く場合はチャットで確認してください。", confirmed_title: "✓ 確定しました", confirmed_hint: "選択内容を保存しました。このページを閉じてチャットに戻ってください。", lang_toggle_title: "言語を切り替え", sec_template_choice: "デザインの基準", template_choice_hint: "この資料のデザインシステムをどう決めるか選択します。", template_free_title: "現在の内容からデザインする", template_free_desc: "再利用テンプレートを使わず、Strategist がこのプロジェクトからビジュアルシステムを組み立てます。", template_use_title: "テンプレートを使用", template_use_desc: "Brand、Style、Layout、Deck、または指定ワークスペースから1つ以上選択します。", sec_template_library: "テンプレートの組み合わせ", template_library_hint: "登録済みテンプレートは種類ごとに1件まで選択でき、Brand、Style、Layout、Deck を組み合わせられます。", sec_template_explicit: "指定テンプレート", template_explicit_hint: "この実行で指定された正確なワークスペースを1件まで選択できます。確認用に参照元パスを表示します。", template_kind_brand: "Brand", template_kind_style: "Style", template_kind_layout: "Layout", template_kind_deck: "Deck", template_source_library: "ライブラリ", template_source_explicit: "指定パス", template_source_path: "参照元パス", template_select_none: "なし", template_none_registered: "登録済みテンプレートはありません", template_none_explicit: "この実行で指定されたテンプレートはありません", sec_canvas: "キャンバス形式", sec_pages: "ページ数", sec_audience: "想定読者", sec_communication: "このプレゼンで何を実現するか", sec_delivery: "どう使い、何を残すか", sec_narrative: "ナラティブ方針", sec_visual: "ビジュアル方針", sec_color: "配色", sec_icons: "アイコンの使用", sec_type: "タイポグラフィ", sec_images: "画像の使用", sec_image_production: "画像制作", sec_proactive_execution: "能動的な実行", sec_mode: "生成モード", sec_refine: "先に設計仕様を確認", sec_design_directions: "統合デザイン方針", design_directions_hint: "おすすめの全体案が最初に適用されています。別案を選ぶか、下の各項目を微調整できます。調整後は「元の案に戻す」で最初の組み合わせを復元できます。", direction_active: "適用中", direction_adjusted: "調整済み", direction_apply_hint: "クリックすると、この全体案を適用します。", direction_restore: "元の案に戻す", scheme_component_options: "プロジェクト専用カスタム案 · カードを選んで編集", sec_template_application: "テンプレートの適用方法", template_application_hint: "AIが現在の内容に合わせたテンプレートの使い方を提案します。自然言語で直接修正できます。", placeholder_template_application: "使用・省略・反復・並べ替えするページやプロトタイプ、保持する要素、差し替え・再構成できる内容を記述します。", sub_mode: "ナラティブモード", sub_visual: "ビジュアルスタイル", sub_divergence: "素材からの発散度(どこまで自由に再構成するか、原文に忠実か)", placeholder_divergence: "自分の言葉でどうぞ — 例:「文書に忠実に」「元素材の範囲内で自由に再構成・展開」。空欄ならバランス型になります。", communication_intent: "このプレゼンで何を実現したいですか?", communication_intent_hint: "自由記述です。情報共有・説明・説得・意思決定・合意形成・教育・報告と説明責任・行動喚起・記録と引き継ぎを必要に応じて組み合わせ、必要なら優先順位や順序も書いてください。ラベルを選ぶ必要はありません。", placeholder_communication_intent: "例:まず進捗とリスクを報告し、そのうえで次の投資判断を得る。", audience_outcome: "聴衆に期待する変化・成功条件", placeholder_audience_outcome: "終了後、聴衆は何を知り、理解し、信じ、決め、行動できる状態になるべきですか?", core_message: "中核メッセージ/意思決定の依頼/行動", placeholder_core_message: "ほかの内容が忘れられても、必ず残すべき主張・依頼・行動は何ですか?", delivery_context: "利用状況(主モードを明記)", delivery_context_hint: "発表者主導、読者主導、ハイブリッド、録画/自動再生を区別してください。ハイブリッドでは主モードと、維持すべき副用途を明記します。", placeholder_delivery_context: "例:主は発表者付き20分の経営レビュー。副は会議後に単独で読む承認資料。", artifact_afterlife: "資料の利用後", placeholder_artifact_afterlife: "例:承認、レビュー、監査、保管、引き継ぎ、再利用。後続利用がなければ空欄で構いません。", stage1_current_value_hint: "編集可能な欄には提案が入っています。そのまま使う・修正する・空にすることができ、確定時の現在値を空欄も含めてそのまま保存します。", content_divergence_locked_hint: "このプロファイルは原文とページ構成を保持するため、この項目は固定されています。", custom: "カスタム", custom_placeholder: "自由に入力…", ai_custom_candidate: "AIカスタム案", ai_custom_candidate_hint: "比較できるよう常に全文を表示します。初期選択はされず、選択後に編集できます。", custom_behavior_required: "選択したAIカスタム案を空欄にはできません。", custom_color_required: "続行する前に、カスタム配色の説明を入力してください。", design_system_required: "続行する前に、完全な配色と書体システムを選択してください。", mode_behavior_placeholder: "構成の流れ、タイトルの語り口、ページのリズム、表現姿勢を記述します。", visual_style_behavior_placeholder: "形状言語、構図、装飾密度、余白、書体の性格、質感を記述します。", recommended: "おすすめ", placeholder_audience: "この資料は誰に向けたもの?", placeholder_pages: "例:12-15", hex_override: "カスタムHEXで上書き:", formula_policy: "数式レンダリング方針", image_ai_path: "AI画像の生成元", image_strategy: "生成画像のスタイル", image_strategy_empty: "プリセットのスタイル見本を利用できません。カスタムスタイルは引き続き使用できます。", image_strategy_required: "生成画像のプリセットを選ぶか、カスタムスタイルを記述してください。", image_strategy_invalid: "選択した生成画像プリセットは利用できません。", image_strategy_select_placeholder: "生成画像のプリセットを選択…", image_strategy_recommended_group: "この資料へのおすすめ", image_strategy_all_group: "すべてのプリセットスタイル", image_strategy_rendering: "レンダリング", image_strategy_visual: "ビジュアル", image_strategy_mood: "ムード", image_strategy_ai_custom: "AIカスタム案", image_strategy_ai_custom_desc: "新規または複数の既存表現を統合したレンダリング案です。選択後に編集できます。", image_strategy_custom_placeholder: "生成画像の方向性、被写体、構図、スタイル要素、避けたい要素を具体的に入力してください。", image_strategy_reference_hint: "参照画像はレンダリングのみを示します。最終AI画像の色は上で選んだデッキ配色を継承します。", image_strategy_no_reference: "このカスタム選択には参照画像がありません。", image_source_summary: "選択中の画像ソース", image_production_hint: "画像ソースとレンダリングは上で選択済みです。ここでは制作経路だけを決めます。", image_usage_notes: "画像に関する補足要件", image_usage_notes_placeholder: "例:リアルな手洗いシーンを優先、漫画調の菌のイラストは避ける、製品写真はそのまま使う。", image_usage_required: "画像の使用方法を少なくとも1つ選択してください。", image_usage_none_exclusive: "「画像なし」は他の画像オプションと同時に選択できません。", proactive_execution_hint: "これらの初期設定は、明示的な指示がない場合にのみ適用されます。最新の明示的な指示が常に優先されます。", proactive_speaker_notes: "発表者ノートを能動的に生成", proactive_speaker_notes_desc: "初期設定はオンです。個別の依頼がなくても発表者ノートを生成します。", proactive_custom_animations: "カスタムアニメーションを能動的に作成", proactive_custom_animations_desc: "初期設定はオフです。ストラテジストの動きの提案は維持され、オンにすると個別の依頼がなくてもカスタムアニメーションを作成します。", proactive_narration_audio: "ナレーション音声を能動的に生成", proactive_narration_audio_desc: "初期設定はオフです。この選択は発表者ノートの設定を書き換えません。ナレーションに必要なノートの有効状態は、ストラテジストが設計仕様で解決します。", font_heading: "見出し", font_body: "本文", font_selection: "フォント選択", primary_language_font: "主要言語のフォント", english_font: "英語フォント", font_picker_hint: "提案を選ぶと下の選択欄に反映されます。いずれかのフォントを変更するとカスタマイズ済みになります。", other_installed_font: "その他のインストール済みフォント…", other_font_placeholder: "インストール済みフォントの正確な名前", customized: "カスタマイズ済み", font_body_size: "本文の基準サイズ", font_body_size_hint: "すべての文字サイズはこの本文基準から導出されます。", body_size_unit_relation: "SVG px と PPT pt の換算:1px = 0.75pt。", body_size_pt_hint: "約 {pt} pt(1px = 0.75pt 換算、保存は px)。", role_size_pt_hint: "約 {pt} pt", body_size_hint_canvas: "このキャンバスの目安は約{lo}–{hi}px(有効キャンバス尺度から算出)。", body_size_hint_purpose: "この閲覧モードの推奨は{def}px — 範囲ではなく固定値です。", body_size_hint_oor: "(現在の値はこのキャンバスの通常範囲外です — 単位とサイズ感を確認してください。)", delivery_purpose: "閲覧モード", delivery_purpose_hint: "情報を主にページと話者のどちらに担わせるかを決めます。近距離閲覧は完全な文と細部で自立させ、プレゼン型は1枚1メッセージで短い主張と視覚的根拠を中心にします。", size_override: "役割ごとのサイズ上書き:", size_role_title: "タイトル", size_role_subtitle: "サブタイトル", size_role_annotation: "注釈", custom_typography: "カスタムタイポグラフィ", custom_color: "カスタム配色", custom_color_placeholder: "配色を言葉で説明 — 例:濃紺をメインに暖色オレンジのアクセント、背景は白 — またはHEX値を貼り付け…", role_background: "背景", role_secondary_bg: "第2背景", role_primary: "メイン", role_accent: "アクセント", role_secondary_accent: "第2アクセント", role_body_text: "本文文字", cjk: "和文", latin: "欧文", sample_heading_cjk: "プレゼンテーションの表題", sample_heading_latin: "Presentation Title", sample_body_cjk: "キーメッセージの要約", sample_body_latin: "Key message summary", style_preview_label: "全体の印象(配色 + タイポグラフィ + アイコン)", style_preview_body: "· 雰囲気の確認用で、実際のレイアウトではありません", no_icons: "アイコンなし", preview_big_title: "大見出し", preview_section_title: "章タイトル", preview_latin_title: "Section Title", preview_body_intro: "本文の基準サイズとコントラストを確認するための文です。", preview_latin_body: "Body text sample for checking Latin typography.", preview_point_1_title: "本文内容", preview_point_1_text: "段落密度と行間の見え方をここで確認します。", preview_point_2_title: "要点説明", preview_point_2_text: "アイコンは単独ではなく、実際の文章の横に配置します。", preview_point_3_title: "結論・提案", preview_point_3_text: "投影時にも読みやすい組み合わせかを判断します。", mode_continuous_desc: "デッキ全体を一気に生成します。", mode_split_desc: "設計仕様の作成後に停止し、別ウィンドウでSVG生成を再開します。", refine_off_desc: "設計仕様と実行ロックを順番に作成し、そのまま自動で進みます。", refine_on_desc: "設計仕様の作成後に停止します。チャットで任意の箇所を修正し、承認後に実行ロックを作成して生成を続けます。", off_default: "オフ", on: "オン", option_prefix: "案", error_retry: "エラー - 再試行" }, zh: { page_title: "确认设计方案", topbar_hint: "回答开放问题,或选择并调整推荐项,然后继续。", stage_anchors: "第一阶段 · 沟通契约", stage_final_plan: "第二阶段 · 最终方案与制作", loading: "加载中…", load_error: "无法加载推荐文件,需在启动前写入。", btn_confirm: "确认", btn_confirm_contract: "确认沟通契约并继续 →", btn_confirm_final_plan: "确认最终方案 →", deriving: "正在根据你的选择生成下游选项…", template_selection_required: "请选择自由设计或使用模板;选择使用模板时,至少选择一个工作区。", connection_lost: "确认页服务连接中断,正在重试;如果持续失败,请回到聊天窗口走聊天确认。", confirmed_title: "✓ 已确认", confirmed_hint: "选择已保存,可关闭此页并回到聊天窗口。", lang_toggle_title: "切换语言", sec_template_choice: "设计基础", template_choice_hint: "选择这份演示文稿如何建立设计系统。", template_free_title: "根据当前内容从零设计", template_free_desc: "不使用可复用模板工作区,由 Strategist 根据当前项目推导视觉系统。", template_use_title: "使用模板", template_use_desc: "选择一个或多个 Brand、Style、Layout、Deck 或指定工作区。", sec_template_library: "模板组合", template_library_hint: "每种已注册模板最多选择一个;Brand、Style、Layout、Deck 可以组合使用。", sec_template_explicit: "指定模板", template_explicit_hint: "本次运行明确提供的精确工作区最多选择一个;显示来源路径供你核对。", template_kind_brand: "Brand", template_kind_style: "Style", template_kind_layout: "Layout", template_kind_deck: "Deck", template_source_library: "模板库", template_source_explicit: "指定地址", template_source_path: "来源路径", template_select_none: "无", template_none_registered: "暂无已注册模板", template_none_explicit: "本次运行没有指定模板", sec_canvas: "画布格式", sec_pages: "页数", sec_audience: "目标受众", sec_communication: "这份演示要完成什么", sec_delivery: "如何使用、之后留下什么", sec_narrative: "叙事方向", sec_visual: "视觉方向", sec_color: "色彩方案", sec_icons: "图标使用", sec_type: "字体方案", sec_images: "图片使用", sec_image_production: "图片生产", sec_proactive_execution: "主动执行", sec_mode: "生成模式", sec_refine: "先审核设计规范", sec_design_directions: "成套设计方向", design_directions_hint: "AI 最倾向的成套方案已默认应用;你可以改选其他方案,或在下方微调各项。调整后可用“恢复原方案”还原整套预设。", direction_active: "已应用", direction_adjusted: "已调整", direction_apply_hint: "点击应用这套完整方案。", direction_restore: "恢复原方案", scheme_component_options: "项目专属自定义方案 · 选中卡片后可编辑", sec_template_application: "模板应用方式", template_application_hint: "AI 会根据当前内容推荐如何使用已安装模板;你可以直接用自然语言修改。", placeholder_template_application: "说明使用、跳过、重复或重排哪些模板页面/原型,哪些内容必须保留,哪些可以替换或重组。", sub_mode: "叙事模式", sub_visual: "视觉风格", sub_divergence: "材料发散度(多大程度重塑,还是贴近源材料)", placeholder_divergence: "用你自己的话写,例如「严格贴着文档来」/「在源材料范围内自由重组并展开」。留空则按平衡处理。", communication_intent: "这份演示文稿需要完成什么?", communication_intent_hint: "开放回答,可按需组合:告知、解释、说服、决策、对齐、教学、汇报与问责、动员、留档与交接。必要时说明主次或先后,不需要选择标签。", placeholder_communication_intent: "例如:先汇报进展并暴露风险,再推动管理层决定下一阶段投入。", audience_outcome: "期望的受众变化 / 成功条件", placeholder_audience_outcome: "结束后,受众应该知道、理解、相信、决定或采取什么行动?", core_message: "核心信息 / 决策请求 / 行动", placeholder_core_message: "即使其他内容没有被记住,受众至少需要接住哪些主张、请求或行动?", delivery_context: "传递场景(明确主要模式)", delivery_context_hint: "区分演讲者主导、读者主导、混合、录制/自动播放;混合场景要说明哪一种主导,以及还要兼顾什么次要用途。", placeholder_delivery_context: "例如:主要为有主讲的 20 分钟管理层现场评审;次要为会后独立阅读的审批材料。", artifact_afterlife: "演示后的成果用途", placeholder_artifact_afterlife: "例如:审批、评审、审计、留档、交接或复用;没有后续用途时可留空。", stage1_current_value_hint: "可编辑字段中是推荐内容。你可以保留、修改或清空;确认时会按当前内容原样保存,空白也会保持为空。", content_divergence_locked_hint: "当前流程要求原文和页面结构保持不变,因此该字段已锁定。", custom: "自定义", custom_placeholder: "输入自定义内容…", ai_custom_candidate: "AI 自定义方案", ai_custom_candidate_hint: "始终展示完整内容用于比较;默认不选中,选择后可编辑。", custom_behavior_required: "已选择的 AI 自定义方案不能为空。", custom_color_required: "请先填写自定义配色说明,再继续确认。", design_system_required: "请先选择完整的配色与字体方案,再继续确认。", mode_behavior_placeholder: "描述叙事阶段、标题语气、页面节奏和表达姿态。", visual_style_behavior_placeholder: "描述形状语言、构图、装饰密度、留白、字体气质和纹理。", recommended: "推荐", placeholder_audience: "这份演示文稿面向谁?", placeholder_pages: "如:12-15", hex_override: "自定义色值覆盖:", formula_policy: "公式渲染策略", image_ai_path: "生成配图来源", image_strategy: "生成图风格", image_strategy_empty: "当前没有可用的预设风格参考,仍可使用自定义风格。", image_strategy_required: "请选择一种生成图预设,或填写自定义风格。", image_strategy_invalid: "所选生成图预设当前不可用。", image_strategy_select_placeholder: "选择生成图预设…", image_strategy_recommended_group: "本项目推荐", image_strategy_all_group: "全部预设风格", image_strategy_rendering: "渲染风格", image_strategy_visual: "视觉", image_strategy_mood: "情绪", image_strategy_ai_custom: "AI 自定义方案", image_strategy_ai_custom_desc: "一套全新或综合多个已有风格的渲染方案;选择后可以编辑。", image_strategy_custom_placeholder: "描述生成图的具体方向、主体、构图、风格关键词或需要避免的内容。", image_strategy_reference_hint: "参考图只展示渲染风格;最终 AI 图片直接继承上方已选的整套 PPT 配色。", image_strategy_no_reference: "自定义选择没有参考图。", image_source_summary: "已选图片来源", image_production_hint: "图片来源和渲染方向已在上方选择;这里仅决定实际生产路径。", image_usage_notes: "图片补充要求", image_usage_notes_placeholder: "例如:优先真实洗手场景;不要卡通病菌;产品照片保持原样。", image_usage_required: "请至少选择一种图片使用方式。", image_usage_none_exclusive: "「不使用图片」不能和其它图片选项同时选择。", proactive_execution_hint: "这些默认开关只在你没有明确要求时生效;你最新的明确指令始终优先。", proactive_speaker_notes: "主动生成演讲者备注", proactive_speaker_notes_desc: "默认开启。无需另行要求,Agent 也会生成演讲者备注。", proactive_custom_animations: "主动生成自定义动画", proactive_custom_animations_desc: "默认关闭。策略师的动画建议仍会保留;开启后,Agent 可在没有另行要求时实际制作自定义动画。", proactive_narration_audio: "主动生成旁白音频", proactive_narration_audio_desc: "默认关闭。这里保留原始选择,不改写演讲者备注开关;策略师会在设计规范中解析旁白所需的最终备注状态。", font_heading: "标题", font_body: "正文", font_selection: "字体选择", primary_language_font: "主要语言字体", english_font: "英文字体", font_picker_hint: "选择推荐方案会同步下方字体;修改任一下拉或手动字体后会标记为已自定义。", other_installed_font: "其他已安装字体…", other_font_placeholder: "输入精确的已安装字体名称", customized: "已自定义", font_body_size: "正文基准字号", font_body_size_hint: "所有字号按这个正文基准推导。", body_size_unit_relation: "SVG px 与 PPT pt 的换算:1px = 0.75pt。", body_size_pt_hint: "约 {pt} pt(按 1px = 0.75pt 换算;提交仍保存 px)。", role_size_pt_hint: "约 {pt} pt", body_size_hint_canvas: "当前画布建议 ~{lo}–{hi}px(按有效画布跨度计算)。", body_size_hint_purpose: "该阅读模式推荐 {def}px(单一固定值,非区间)。", body_size_hint_oor: "(当前数值超出该画布的常用范围——请确认单位无误、是否合适。)", delivery_purpose: "阅读模式", delivery_purpose_hint: "决定信息主要由页面还是讲者承担:近读型用完整句、短段落和细节自洽;演讲型一页一意,以简短主张和视觉证据为主。", size_override: "逐角色字号覆盖:", size_role_title: "标题", size_role_subtitle: "副标题", size_role_annotation: "注释", custom_typography: "自定义字体方案", custom_color: "自定义配色", custom_color_placeholder: "用文字描述配色,如:深蓝主色、暖橙强调、白色背景——或直接粘贴 HEX 值…", role_background: "背景", role_secondary_bg: "次级背景", role_primary: "主色", role_accent: "强调", role_secondary_accent: "次强调", role_body_text: "正文文字", cjk: "中文", latin: "西文", sample_heading_cjk: "主题方案标题", sample_heading_latin: "Presentation Title", sample_body_cjk: "关键信息摘要", sample_body_latin: "Key message summary", style_preview_label: "整体形象(配色 + 字体 + 图标)", style_preview_body: "· 仅大致形象,非实际版式", no_icons: "无图标", preview_big_title: "大标题", preview_section_title: "章节标题", preview_latin_title: "Section Title", preview_body_intro: "正文内容用于判断基础字号、行距和颜色对比。", preview_latin_body: "Body text sample for checking Latin typography.", preview_point_1_title: "正文内容", preview_point_1_text: "这里展示普通段落的密度和阅读节奏。", preview_point_2_title: "要点说明", preview_point_2_text: "图标和文字放在一起,判断真实使用效果。", preview_point_3_title: "结论建议", preview_point_3_text: "组合效果需要在演示场景下保持清晰可读。", mode_continuous_desc: "一次性连续生成整份演示文稿。", mode_split_desc: "写完设计规范后停止,另开窗口继续生成页面。", refine_off_desc: "依次生成设计规范和执行锁,然后自动继续。", refine_on_desc: "生成设计规范后暂停;你可在聊天中修改任何部分,明确确认后再生成执行锁并继续制作。", off_default: "关", on: "开", option_prefix: "方案", error_retry: "出错,请重试" }, "zh-TW": { page_title: "確認設計方案", topbar_hint: "回答開放問題,或選擇並調整推薦項,然後繼續。", stage_anchors: "第一階段 · 溝通契約", stage_final_plan: "第二階段 · 最終方案與製作", loading: "載入中…", load_error: "無法載入推薦檔案,需在啟動前寫入。", btn_confirm: "確認", btn_confirm_contract: "確認溝通契約並繼續 →", btn_confirm_final_plan: "確認最終方案 →", deriving: "正在根據你的選擇生成下游選項…", template_selection_required: "請選擇自由設計或使用範本;選擇使用範本時,至少選擇一個工作區。", connection_lost: "確認頁服務連線中斷,正在重試;如果持續失敗,請回到聊天視窗走聊天確認。", confirmed_title: "✓ 已確認", confirmed_hint: "選擇已儲存,可關閉此頁並回到聊天視窗。", lang_toggle_title: "切換語言", sec_template_choice: "設計基礎", template_choice_hint: "選擇這份簡報如何建立設計系統。", template_free_title: "根據目前內容從零設計", template_free_desc: "不使用可重複使用的範本工作區,由 Strategist 根據目前專案推導視覺系統。", template_use_title: "使用範本", template_use_desc: "選擇一個或多個 Brand、Style、Layout、Deck 或指定工作區。", sec_template_library: "範本組合", template_library_hint: "每種已註冊範本最多選擇一個;Brand、Style、Layout、Deck 可以組合使用。", sec_template_explicit: "指定範本", template_explicit_hint: "本次執行明確提供的精確工作區最多選擇一個;顯示來源路徑供你核對。", template_kind_brand: "Brand", template_kind_style: "Style", template_kind_layout: "Layout", template_kind_deck: "Deck", template_source_library: "範本庫", template_source_explicit: "指定地址", template_source_path: "來源路徑", template_select_none: "無", template_none_registered: "尚無已註冊範本", template_none_explicit: "本次執行沒有指定範本", sec_canvas: "畫布格式", sec_pages: "頁數", sec_audience: "目標受眾", sec_communication: "這份簡報要完成什麼", sec_delivery: "如何使用、之後留下什麼", sec_narrative: "敘事方向", sec_visual: "視覺方向", sec_color: "色彩方案", sec_icons: "圖示使用", sec_type: "字型方案", sec_images: "圖片使用", sec_image_production: "圖片產製", sec_proactive_execution: "主動執行", sec_mode: "生成模式", sec_refine: "先審閱設計規範", sec_design_directions: "成套設計方向", design_directions_hint: "AI 最傾向的成套方案已預設套用;你可以改選其他方案,或在下方微調各項。調整後可用「還原原始方案」還原整套預設。", direction_active: "已套用", direction_adjusted: "已調整", direction_apply_hint: "按一下即可套用這套完整方案。", direction_restore: "還原原始方案", scheme_component_options: "專案專屬自訂方案 · 選取卡片後可編輯", sec_template_application: "範本套用方式", template_application_hint: "AI 會根據目前內容推薦如何使用已安裝範本;你可以直接用自然語言修改。", placeholder_template_application: "說明使用、跳過、重複或重排哪些範本頁面/原型,哪些內容必須保留,哪些可以替換或重組。", sub_mode: "敘事模式", sub_visual: "視覺風格", sub_divergence: "材料發散度(多大程度重塑,還是貼近源材料)", placeholder_divergence: "用你自己的話寫,例如「嚴格貼著文件來」/「在源材料範圍內自由重組並展開」。留空則按平衡處理。", communication_intent: "這份簡報需要完成什麼?", communication_intent_hint: "開放回答,可按需組合:告知、解釋、說服、決策、對齊、教學、報告與問責、動員、留檔與交接。必要時說明主次或先後,不需要選擇標籤。", placeholder_communication_intent: "例如:先報告進展並暴露風險,再推動管理階層決定下一階段投入。", audience_outcome: "期望的受眾變化 / 成功條件", placeholder_audience_outcome: "結束後,受眾應該知道、理解、相信、決定或採取什麼行動?", core_message: "核心資訊 / 決策請求 / 行動", placeholder_core_message: "即使其他內容沒有被記住,受眾至少需要接住哪些主張、請求或行動?", delivery_context: "傳遞場景(明確主要模式)", delivery_context_hint: "區分演講者主導、讀者主導、混合、錄製/自動播放;混合場景要說明哪一種主導,以及還要兼顧什麼次要用途。", placeholder_delivery_context: "例如:主要為有主講的 20 分鐘管理階層現場評審;次要為會後獨立閱讀的簽核文件。", artifact_afterlife: "簡報後的成果用途", placeholder_artifact_afterlife: "例如:簽核、評審、稽核、留檔、交接或重複使用;沒有後續用途時可留空。", stage1_current_value_hint: "可編輯欄位中是推薦內容。你可以保留、修改或清空;確認時會按目前內容原樣儲存,空白也會保持為空。", content_divergence_locked_hint: "目前流程要求原文和頁面結構保持不變,因此該欄位已鎖定。", custom: "自訂", custom_placeholder: "輸入自訂內容…", ai_custom_candidate: "AI 自訂方案", ai_custom_candidate_hint: "始終展示完整內容用於比較;預設不選取,選擇後可編輯。", custom_behavior_required: "已選擇的 AI 自訂方案不能為空。", custom_color_required: "請先填寫自訂配色說明,再繼續確認。", design_system_required: "請先選擇完整的配色與字型方案,再繼續確認。", mode_behavior_placeholder: "描述敘事階段、標題語氣、頁面節奏和表達姿態。", visual_style_behavior_placeholder: "描述形狀語言、構圖、裝飾密度、留白、字型氣質和紋理。", recommended: "推薦", placeholder_audience: "這份簡報面向誰?", placeholder_pages: "如:12-15", hex_override: "自訂色值覆蓋:", formula_policy: "公式渲染策略", image_ai_path: "生成配圖來源", image_strategy: "生成圖風格", image_strategy_empty: "目前沒有可用的預設風格參考,仍可使用自訂風格。", image_strategy_required: "請選擇一種生成圖預設,或填寫自訂風格。", image_strategy_invalid: "所選生成圖預設目前不可用。", image_strategy_select_placeholder: "選擇生成圖預設…", image_strategy_recommended_group: "本專案推薦", image_strategy_all_group: "全部預設風格", image_strategy_rendering: "渲染風格", image_strategy_visual: "視覺", image_strategy_mood: "情緒", image_strategy_ai_custom: "AI 自訂方案", image_strategy_ai_custom_desc: "一套全新或綜合多個已有風格的渲染方案;選擇後可以編輯。", image_strategy_custom_placeholder: "描述生成圖的具體方向、主體、構圖、風格關鍵字或需要避免的內容。", image_strategy_reference_hint: "參考圖只展示渲染風格;最終 AI 圖片直接繼承上方已選的整套 PPT 配色。", image_strategy_no_reference: "自訂選擇沒有參考圖。", image_source_summary: "已選圖片來源", image_production_hint: "圖片來源和渲染方向已在上方選擇;這裡僅決定實際產製路徑。", image_usage_notes: "圖片補充要求", image_usage_notes_placeholder: "例如:優先真實洗手場景;不要卡通病菌;產品照片保持原樣。", image_usage_required: "請至少選擇一種圖片使用方式。", image_usage_none_exclusive: "「不使用圖片」不能和其他圖片選項同時選擇。", proactive_execution_hint: "這些預設開關只在你沒有明確要求時生效;你最新的明確指令始終優先。", proactive_speaker_notes: "主動生成演講者備註", proactive_speaker_notes_desc: "預設開啟。無需另行要求,Agent 也會生成演講者備註。", proactive_custom_animations: "主動生成自訂動畫", proactive_custom_animations_desc: "預設關閉。策略師的動畫建議仍會保留;開啟後,Agent 可在沒有另行要求時實際製作自訂動畫。", proactive_narration_audio: "主動生成旁白音訊", proactive_narration_audio_desc: "預設關閉。這裡保留原始選擇,不改寫演講者備註開關;策略師會在設計規範中解析旁白所需的最終備註狀態。", font_heading: "標題", font_body: "正文", font_selection: "字型選擇", primary_language_font: "主要語言字型", english_font: "英文字型", font_picker_hint: "選擇推薦方案會同步下方字型;修改任一下拉或手動字型後會標記為已自訂。", other_installed_font: "其他已安裝字型…", other_font_placeholder: "輸入精確的已安裝字型名稱", customized: "已自訂", font_body_size: "正文基準字級", font_body_size_hint: "所有字級按這個正文基準推導。", body_size_unit_relation: "SVG px 與 PPT pt 的換算:1px = 0.75pt。", body_size_pt_hint: "約 {pt} pt(按 1px = 0.75pt 換算;提交仍儲存 px)。", role_size_pt_hint: "約 {pt} pt", body_size_hint_canvas: "目前畫布建議 ~{lo}–{hi}px(依有效畫布跨距計算)。", body_size_hint_purpose: "該閱讀模式推薦 {def}px(單一固定值,非區間)。", body_size_hint_oor: "(目前數值超出該畫布的常用範圍——請確認單位無誤、是否合適。)", delivery_purpose: "閱讀模式", delivery_purpose_hint: "決定資訊主要由頁面還是講者承擔:近讀型用完整句、短段落和細節自洽;演講型一頁一意,以簡短主張和視覺證據為主。", size_override: "逐角色字級覆蓋:", size_role_title: "標題", size_role_subtitle: "副標題", size_role_annotation: "註解", custom_typography: "自訂字型方案", custom_color: "自訂配色", custom_color_placeholder: "用文字描述配色,如:深藍主色、暖橙強調、白色背景——或直接貼上 HEX 值…", role_background: "背景", role_secondary_bg: "次級背景", role_primary: "主色", role_accent: "強調", role_secondary_accent: "次強調", role_body_text: "正文文字", cjk: "中文", latin: "西文", sample_heading_cjk: "主題方案標題", sample_heading_latin: "Presentation Title", sample_body_cjk: "關鍵資訊摘要", sample_body_latin: "Key message summary", style_preview_label: "整體形象(配色 + 字型 + 圖示)", style_preview_body: "· 僅大致形象,非實際版式", no_icons: "無圖示", preview_big_title: "大標題", preview_section_title: "章節標題", preview_latin_title: "Section Title", preview_body_intro: "正文內容用於判斷基礎字級、行距和顏色對比。", preview_latin_body: "Body text sample for checking Latin typography.", preview_point_1_title: "正文內容", preview_point_1_text: "這裡展示普通段落的密度和閱讀節奏。", preview_point_2_title: "要點說明", preview_point_2_text: "圖示和文字放在一起,判斷真實使用效果。", preview_point_3_title: "結論建議", preview_point_3_text: "組合效果需要在簡報場景下保持清晰可讀。", mode_continuous_desc: "一次性連續生成整份簡報。", mode_split_desc: "寫完設計規範後停止,另開視窗繼續生成頁面。", refine_off_desc: "依次生成設計規範和執行鎖,然後自動繼續。", refine_on_desc: "生成設計規範後暫停;你可在聊天中修改任何部分,明確確認後再生成執行鎖並繼續製作。", off_default: "關", on: "開", option_prefix: "方案", error_retry: "出錯,請重試" } }; var LANG = (function () { try { var stored = window.localStorage.getItem("ppt_lang"); if (stored === "zh" || stored === "en" || stored === "ja" || stored === "zh-TW") return stored; } catch (e) { /* ignore */ } var nav = (navigator.language || navigator.userLanguage || "en").toLowerCase(); if (nav.indexOf("zh") === 0) { if (/\bhans\b/.test(nav)) return "zh"; if (/\bhant\b/.test(nav) || /\b(tw|hk|mo)\b/.test(nav)) return "zh-TW"; return "zh"; } if (nav.indexOf("ja") === 0) return "ja"; return "en"; })(); function t(key) { var dict = MESSAGES[LANG] || MESSAGES.en; return dict[key] != null ? dict[key] : key; } // Preserve the existing locale order, then accept zh_tw-only candidate // prose from any persisted UI language so browser and server validation agree. // Entries are FIELD SUFFIXES, not BCP-47 tags: "zh-TW" data lives in // `_zh_tw` keys, so a hyphenless suffix is used here and in langField(). var LANG_FALLBACK = { zh: ["zh", "en", "ja", "zh_tw"], en: ["en", "zh", "ja", "zh_tw"], ja: ["ja", "en", "zh", "zh_tw"], "zh-TW": ["zh_tw", "zh", "en", "ja"] }; // Suffix used to look up localized catalog/recommendation fields. var LANG_FIELD = { "zh-TW": "zh_tw" }; function langField(lang) { return LANG_FIELD[lang] || lang; } var IMAGE_COMPARISON_LABELS = { rendering: { "vector-illustration": { zh: "矢量插画", zh_tw: "向量插畫", en: "Vector illustration", ja: "ベクターイラスト" }, flat: { zh: "扁平插画", zh_tw: "扁平插畫", en: "Flat illustration", ja: "フラットイラスト" }, "3d-isometric": { zh: "3D 等距", zh_tw: "3D 等距", en: "3D isometric", ja: "3Dアイソメトリック" }, "digital-dashboard": { zh: "数字仪表盘", zh_tw: "數位儀表板", en: "Digital dashboard", ja: "デジタルダッシュボード" }, "corporate-photo": { zh: "企业摄影", zh_tw: "企業攝影", en: "Corporate photo", ja: "企業写真" }, blueprint: { zh: "蓝图线稿", zh_tw: "藍圖線稿", en: "Blueprint", ja: "ブループリント" }, editorial: { zh: "编辑杂志", zh_tw: "編輯雜誌", en: "Editorial", ja: "エディトリアル" }, "sketch-notes": { zh: "手绘笔记", zh_tw: "手繪筆記", en: "Sketch notes", ja: "スケッチノート" }, "ink-notes": { zh: "墨线笔记", zh_tw: "墨線筆記", en: "Ink notes", ja: "インクノート" }, chalkboard: { zh: "粉笔黑板", zh_tw: "粉筆黑板", en: "Chalkboard", ja: "チョークボード" }, watercolor: { zh: "水彩", zh_tw: "水彩", en: "Watercolor", ja: "水彩" }, "warm-scene": { zh: "暖调场景", zh_tw: "暖調場景", en: "Warm scene", ja: "暖色シーン" }, "screen-print": { zh: "丝网印刷", zh_tw: "絲網印刷", en: "Screen print", ja: "スクリーンプリント" }, "fantasy-animation": { zh: "幻想动画", zh_tw: "幻想動畫", en: "Fantasy animation", ja: "ファンタジーアニメ" }, "pixel-art": { zh: "像素艺术", zh_tw: "像素藝術", en: "Pixel art", ja: "ピクセルアート" }, nature: { zh: "自然有机", zh_tw: "自然有機", en: "Nature", ja: "自然・オーガニック" }, "minimalist-swiss": { zh: "瑞士极简", zh_tw: "瑞士極簡", en: "Minimalist Swiss", ja: "スイスミニマル" }, glassmorphism: { zh: "玻璃拟态", zh_tw: "玻璃擬態", en: "Glassmorphism", ja: "グラスモーフィズム" }, "vintage-poster": { zh: "复古海报", zh_tw: "復古海報", en: "Vintage poster", ja: "ヴィンテージポスター" }, "paper-cut": { zh: "剪纸拼贴", zh_tw: "剪紙拼貼", en: "Paper cut", ja: "ペーパーカット" } } }; function localized(obj, base) { if (!obj) return ""; var langKey = base + "_" + langField(LANG); if (obj[langKey] != null) return obj[langKey]; var order = LANG_FALLBACK[LANG] || LANG_FALLBACK.en; var i; if (obj[base] != null) { if (typeof obj[base] === "object") { for (i = 0; i < order.length; i++) { if (obj[base][order[i]]) return obj[base][order[i]]; } return ""; } return obj[base]; } for (i = 0; i < order.length; i++) { if (obj[base + "_" + order[i]]) return obj[base + "_" + order[i]]; } return ""; } function optionLabel(option) { return localized(option, "label") || String(option && option.id); } function optionDesc(option) { return localized(option, "desc"); } function groupLabel(group) { return localized(group, "group"); } function humanizeId(value) { return String(value || "") .replace(/[_-]+/g, " ") .replace(/\b[a-z]/g, function (match) { return match.toUpperCase(); }); } function langMappedLabel(kind, id) { if (!id) return ""; if (id === "custom") return t("custom"); var entry = IMAGE_COMPARISON_LABELS[kind] && IMAGE_COMPARISON_LABELS[kind][id]; if (!entry) return ""; var order = LANG_FALLBACK[LANG] || LANG_FALLBACK.en; for (var i = 0; i < order.length; i += 1) { if (entry[order[i]]) return entry[order[i]]; } return entry.en || ""; } function comparisonValueLabel(kind, id) { return langMappedLabel(kind, id) || humanizeId(id); } function applyStaticTranslations() { document.documentElement.setAttribute("lang", LANG === "zh" ? "zh-CN" : (LANG === "zh-TW" ? "zh-TW" : (LANG === "ja" ? "ja" : "en"))); document.querySelectorAll("[data-i18n]").forEach(function (node) { node.textContent = t(node.getAttribute("data-i18n")); }); } var LANG_NAMES = { zh: "中文", en: "English", ja: "日本語", "zh-TW": "繁體中文" }; function refreshLangToggle(toggleBtn) { // Custom dropdown (OS-independent): button shows the CURRENT language. var cur = document.getElementById("lang-current"); if (cur) cur.textContent = LANG_NAMES[LANG] || LANG; toggleBtn.title = t("lang_toggle_title"); document.querySelectorAll("#lang-menu li").forEach(function (li) { var selected = li.getAttribute("data-lang") === LANG; li.classList.toggle("selected", selected); li.setAttribute("aria-selected", selected ? "true" : "false"); }); } // ---- state ----------------------------------------------------------- var CAT = null; // catalogs.json — finite option universe var REC = null; // current recommendation stage — AI picks + candidates var ICON_PREVIEWS = {}; // /api/icon-previews — real SVG samples from templates/icons var AI_IMAGE_COMPARISON = {}; // /api/ai-image-comparison — preset rendering catalog var STATE = {}; var ACTIVE_DIRECTION_ID = ""; var ACTIVE_DIRECTION_BASELINE = ""; var ACTIVE_COMPONENT_DIRECTION_IDS = {}; var refreshDesignDirectionState = function () {}; var DIRECTION_COMPONENT_PAINTERS = []; function refreshDirectionComponentStates() { DIRECTION_COMPONENT_PAINTERS.forEach(function (paint) { paint(); }); } var TEMPLATE_KINDS = ["brand", "style", "layout", "deck"]; var TEMPLATE_OPTIONS = null; var TEMPLATE_CANDIDATES = []; var TEMPLATE_SELECTED_KEYS = []; var TEMPLATE_SELECTIONS = { brand: "", style: "", layout: "", deck: "", explicit: "" }; var TEMPLATE_MODE = ""; var REC_ALIASES = { icons: { line: "tabler-outline", filled: "tabler-filled", monochrome: "chunk-filled" }, image_usage: { search: "web" }, image_ai_path: { default: "auto", builtin: "host-native" } }; // ---- DOM helpers ----------------------------------------------------- function el(tag, cls, text) { var node = document.createElement(tag); if (cls) node.className = cls; if (text != null) node.textContent = text; return node; } function fitTextareaToContent(input) { if (!input) return; window.requestAnimationFrame(function () { if (!input.isConnected || input.offsetParent === null) return; input.style.height = "auto"; var borderHeight = input.offsetHeight - input.clientHeight; input.style.height = (input.scrollHeight + borderHeight) + "px"; }); } function previewNode(kind, id) { var node = el("div", "option-preview option-preview-" + kind); node.setAttribute("aria-hidden", "true"); if (kind === "visual_style") { appendVisualStyleImage(node, id); return node; } var markup = kind === "icons" ? iconStylePreview(id) : ""; if (!markup) return null; node.innerHTML = markup; return node; } function visualStylePreviewSrc(id) { return "/static/style_previews/" + encodeURIComponent(id || "") + ".svg"; } function appendVisualStyleImage(parent, id) { var img = document.createElement("img"); img.alt = ""; img.loading = "lazy"; img.src = visualStylePreviewSrc(id); img.onerror = function () { parent.innerHTML = visualStylePreview(id); }; parent.appendChild(img); } function escapeHtml(value) { return String(value == null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function visualStylePreview(id) { var label = escapeHtml(humanizeId(id) || t("sub_visual")); var fallback = escapeHtml(t("error_retry")); return '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + label + '' + '' + fallback + '' + ''; } function iconStylePreview(id) { var common = 'xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100"'; var samples = ICON_PREVIEWS[id] || []; if (samples.length) { var sample = samples[0] || {}; return '
' + '
' + (sample.svg || "") + '
' + escapeHtml(sample.name || "") + '
' + '
'; } if (id === "emoji") return '📊'; if (id === "none") return ''; return ""; } function comparisonImageUrl(kind, id) { var value = String(id || "").trim(); if (!value || value === "custom") return ""; if (!/^[A-Za-z0-9_.-]+$/.test(value)) return ""; return "/ai-image-comparison/" + kind + "/" + encodeURIComponent(value) + ".png"; } function appendImageStrategyPreviews(card, candidate) { if (candidate.rendering === "custom") return; var previews = [ [t("image_strategy_rendering"), comparisonImageUrl("rendering", candidate.rendering)] ].filter(function (item) { return item[1]; }); if (!previews.length) return; var row = el("div", "image-strategy-previews"); previews.forEach(function (item) { var frame = el("div", "image-strategy-preview"); var img = document.createElement("img"); img.alt = item[0]; img.loading = "lazy"; img.src = item[1]; img.onerror = function () { frame.remove(); if (!row.childElementCount) row.remove(); }; frame.appendChild(img); frame.appendChild(el("span", "image-strategy-preview-label", item[0])); row.appendChild(frame); }); card.appendChild(row); return row; } // Section numbers run 1..N within the stage currently rendered; the counter is // reset at the top of renderForStage. The legacy `num` arg is ignored so each // stage numbers its own sections cleanly (stage 2 is not a continuation of 1). var _secCounter = 0; function section(num, titleKey, noteText) { _secCounter += 1; var sec = el("div", "section"); var head = el("div", "section-head"); head.appendChild(el("span", "section-num", String(_secCounter))); head.appendChild(el("span", "section-title", t(titleKey))); if (noteText) head.appendChild(el("span", "section-note", noteText)); sec.appendChild(head); return sec; } function setSectionNote(sec, text) { var head = sec.querySelector(".section-head"); var note = head.querySelector(".section-note"); if (!note) { note = el("span", "section-note"); head.appendChild(note); } note.textContent = text; } // ---- Stage 1 template selection -------------------------------------- function normalizeTemplateCandidates(raw, source, kind) { if (!Array.isArray(raw)) return []; return raw.filter(function (candidate) { return candidate && candidate.key != null && String(candidate.key).trim(); }).map(function (candidate) { var normalized = Object.assign({}, candidate); normalized.key = String(candidate.key); normalized.source = source; if (kind) normalized.kind = kind; return normalized; }); } function initTemplateOptions(data) { var library = data && data.library && typeof data.library === "object" ? data.library : {}; var defaultMode = data && data.default_mode; if (defaultMode !== "free_design" && defaultMode !== "templates") { throw new Error("template_options.default_mode must be free_design or templates"); } var normalized = { lang: data && data.lang, default_mode: defaultMode, library: {}, explicit: [], preselected_keys: Array.isArray(data && data.preselected_keys) ? data.preselected_keys.map(String) : [] }; TEMPLATE_CANDIDATES = []; TEMPLATE_KINDS.forEach(function (kind) { var candidates = normalizeTemplateCandidates(library[kind], "library", kind); normalized.library[kind] = candidates; TEMPLATE_CANDIDATES = TEMPLATE_CANDIDATES.concat(candidates); }); normalized.explicit = normalizeTemplateCandidates(data && data.explicit, "explicit", ""); TEMPLATE_CANDIDATES = TEMPLATE_CANDIDATES.concat(normalized.explicit); var candidatesByKey = Object.create(null); TEMPLATE_CANDIDATES.forEach(function (candidate) { if (candidatesByKey[candidate.key]) { throw new Error("Duplicate template option key: " + candidate.key); } candidatesByKey[candidate.key] = candidate; }); TEMPLATE_SELECTIONS = emptyTemplateSelections(); normalized.preselected_keys.filter(function (key, index, all) { return all.indexOf(key) === index; }).forEach(function (key) { var candidate = candidatesByKey[key]; var slot = templateSelectionSlot(candidate); if (!candidate || !slot) { throw new Error("Invalid preselected template key: " + key); } if (TEMPLATE_SELECTIONS[slot]) { throw new Error("Multiple preselected templates for slot: " + slot); } TEMPLATE_SELECTIONS[slot] = key; }); syncTemplateSelectionState(); TEMPLATE_MODE = normalized.default_mode; TEMPLATE_OPTIONS = normalized; } function emptyTemplateSelections() { return { brand: "", style: "", layout: "", deck: "", explicit: "" }; } function templateSelectionSlot(candidate) { if (!candidate) return ""; if (candidate.source === "explicit") return "explicit"; if (candidate.source === "library" && TEMPLATE_KINDS.indexOf(candidate.kind) >= 0) { return candidate.kind; } return ""; } function templateCandidateByKey(key) { key = String(key || ""); for (var i = 0; i < TEMPLATE_CANDIDATES.length; i += 1) { if (TEMPLATE_CANDIDATES[i].key === key) return TEMPLATE_CANDIDATES[i]; } return null; } function syncTemplateSelectionState() { TEMPLATE_SELECTED_KEYS = TEMPLATE_KINDS.map(function (kind) { return TEMPLATE_SELECTIONS[kind]; }); TEMPLATE_SELECTED_KEYS.push(TEMPLATE_SELECTIONS.explicit); TEMPLATE_SELECTED_KEYS = TEMPLATE_SELECTED_KEYS.filter(Boolean); } function templateCandidateTitle(candidate) { return localized(candidate, "label") || candidate.label || candidate.id || candidate.key; } function templateCandidateSummary(candidate) { return localized(candidate, "summary") || candidate.summary || ""; } function templateKindLabel(kind) { return t("template_kind_" + kind); } function chooseFreeDesign() { TEMPLATE_SELECTIONS = emptyTemplateSelections(); syncTemplateSelectionState(); TEMPLATE_MODE = "free_design"; updateTemplateSelectionControls(); } function chooseTemplateMode() { TEMPLATE_MODE = "templates"; updateTemplateSelectionControls(); } function chooseTemplateForSlot(slot, key) { TEMPLATE_SELECTIONS[slot] = String(key || ""); syncTemplateSelectionState(); TEMPLATE_MODE = "templates"; updateTemplateSelectionControls(); } function templateOptionTitle(candidate) { var details = []; var summary = templateCandidateSummary(candidate); if (summary) details.push(summary); if (candidate.workspace_root) details.push(candidate.workspace_root); return details.join(" · "); } function renderTemplateSelectField(slot, label, candidates) { var field = el("div", "template-select-field template-select-field-" + slot); var selectId = "template-select-" + slot; var fieldLabel = el("label", "template-select-label", label); fieldLabel.setAttribute("for", selectId); field.appendChild(fieldLabel); var select = el("select", "template-select"); select.id = selectId; select.setAttribute("data-template-slot", slot); var none = el("option", "", t("template_select_none")); none.value = ""; select.appendChild(none); candidates.forEach(function (candidate) { var option = el("option", "", templateCandidateTitle(candidate)); option.value = candidate.key; var title = templateOptionTitle(candidate); if (title) option.title = title; select.appendChild(option); }); select.value = TEMPLATE_SELECTIONS[slot] || ""; select.disabled = !candidates.length; select.addEventListener("change", function () { chooseTemplateForSlot(slot, select.value); }); field.appendChild(select); if (slot === "explicit") { field.appendChild(el( "div", "template-select-help", candidates.length ? t("template_explicit_hint") : t("template_none_explicit") )); var path = el("div", "template-selected-path"); path.id = "template-explicit-path"; path.appendChild(el("span", "template-selected-path-label", t("template_source_path") + ":")); var code = el("code", "template-selected-path-value"); code.id = "template-explicit-path-value"; path.appendChild(code); field.appendChild(path); } return field; } function renderTemplateModeChoice(id, mode, titleKey, descKey, onSelect) { var selected = TEMPLATE_MODE === mode; var choice = el("button", "template-mode-choice" + (selected ? " selected" : "")); choice.id = id; choice.type = "button"; choice.setAttribute("aria-pressed", selected ? "true" : "false"); choice.appendChild(el("span", "template-choice-radio")); var copy = el("span", "template-choice-copy"); copy.appendChild(el("span", "template-choice-title", t(titleKey))); copy.appendChild(el("span", "template-choice-desc", t(descKey))); choice.appendChild(copy); choice.addEventListener("click", onSelect); return choice; } function renderTemplateSelection(host) { if (!TEMPLATE_OPTIONS) return; var sec = section("T", "sec_template_choice", t("template_choice_hint")); var choices = el("div", "template-mode-choices"); choices.appendChild(renderTemplateModeChoice( "template-free-choice", "free_design", "template_free_title", "template_free_desc", chooseFreeDesign )); var useChoice = renderTemplateModeChoice( "template-use-choice", "templates", "template_use_title", "template_use_desc", chooseTemplateMode ); useChoice.setAttribute("aria-controls", "template-selector-panel"); choices.appendChild(useChoice); sec.appendChild(choices); var panel = el("div", "template-selector-panel"); panel.id = "template-selector-panel"; panel.appendChild(el("div", "template-selector-hint", t("template_library_hint"))); var grid = el("div", "template-select-grid"); TEMPLATE_KINDS.forEach(function (kind) { var candidates = TEMPLATE_OPTIONS.library[kind] || []; grid.appendChild(renderTemplateSelectField(kind, templateKindLabel(kind), candidates)); }); grid.appendChild(renderTemplateSelectField( "explicit", t("template_source_explicit"), TEMPLATE_OPTIONS.explicit || [] )); panel.appendChild(grid); sec.appendChild(panel); host.appendChild(sec); updateTemplateSelectionControls(); } function updateTemplateExplicitPath() { var path = document.getElementById("template-explicit-path"); var value = document.getElementById("template-explicit-path-value"); if (!path || !value) return; var candidate = templateCandidateByKey(TEMPLATE_SELECTIONS.explicit); var workspaceRoot = candidate && candidate.workspace_root ? candidate.workspace_root : ""; path.hidden = !workspaceRoot; value.textContent = workspaceRoot; value.title = workspaceRoot; } function updateTemplateSelectionControls() { var freeChoice = document.getElementById("template-free-choice"); var useChoice = document.getElementById("template-use-choice"); var selectorPanel = document.getElementById("template-selector-panel"); var freeSelected = TEMPLATE_MODE === "free_design"; var templatesSelected = TEMPLATE_MODE === "templates"; if (freeChoice) { freeChoice.classList.toggle("selected", freeSelected); freeChoice.setAttribute("aria-pressed", freeSelected ? "true" : "false"); } if (useChoice) { useChoice.classList.toggle("selected", templatesSelected); useChoice.setAttribute("aria-pressed", templatesSelected ? "true" : "false"); useChoice.setAttribute("aria-expanded", templatesSelected ? "true" : "false"); } if (selectorPanel) selectorPanel.hidden = !templatesSelected; TEMPLATE_KINDS.concat(["explicit"]).forEach(function (slot) { var select = document.getElementById("template-select-" + slot); if (select) select.value = TEMPLATE_SELECTIONS[slot] || ""; }); updateTemplateExplicitPath(); var status = document.getElementById("confirm-status"); if (status) status.textContent = ""; } function normalizeRecId(field, value) { if (Array.isArray(value)) return normalizeRecId(field, value[0]); if (value == null || value === "") return value; var aliases = REC_ALIASES[field] || {}; return aliases[value] || value; } function normalizeRecIds(field, value) { if (Array.isArray(value)) { return value.map(function (item) { return normalizeRecId(field, item); }) .filter(function (item, idx, arr) { return item && arr.indexOf(item) === idx; }); } var normalized = normalizeRecId(field, value); return normalized ? [normalized] : []; } function legacyRecId(field) { if (!REC) return null; if (field === "canvas") return REC.canvas && REC.canvas.value; if (field === "visual_style") return REC.visual_style || (REC.style && REC.style.value); if (field === "icons") return REC.icons && REC.icons.value; if (field === "image_usage") return REC.images && REC.images.value; if (field === "image_ai_path") return REC.image_ai_path || (REC.images && REC.images.ai_path); if (field === "formula_policy") return REC.typography && REC.typography.formula_policy && REC.typography.formula_policy.value; if (field === "generation_mode") return REC.generation_mode && REC.generation_mode.value; return REC[field] && REC[field].value; } function recId(field) { var value = (REC && REC.recommend && REC.recommend[field]) || legacyRecId(field); return normalizeRecId(field, value || null); } function recValue(field) { return (REC && REC.recommend && REC.recommend[field]) || legacyRecId(field); } function booleanRecommendation(field, fallback) { if (!REC || !Object.prototype.hasOwnProperty.call(REC, field)) return fallback; var spec = REC[field]; var value = spec && typeof spec === "object" ? spec.value : spec; return typeof value === "boolean" ? value : fallback; } function recommendationFieldLocked(field) { return !!(REC && REC[field] && typeof REC[field] === "object" && REC[field].locked === true); } // Guaranteed recommendation: the AI's pick, or the first catalog option as a // fallback so an enumerable field ALWAYS shows a badged recommendation. function recOrFirst(field, list) { var r = recId(field); if (r == null || r === "") r = normalizeRecId(field, directionField(field)); if (r != null && r !== "") return r; return firstId(list); } // Render an enumerable field: ALL options from the catalog, recommended one // badged, current selection from STATE, plus an optional Custom box. An // AI-authored custom candidate stays fully visible while unselected and // becomes editable only after selection; legacy custom inputs keep their // compact free-text behavior. // `list` is either a flat array of {id,label,desc,dim,viewbox} or a grouped array // of {group, items:[...]}. function enumField(parent, list, recommendedId, getVal, setVal, opts2) { list = list || []; opts2 = opts2 || {}; var grouped = list.length && list[0] && list[0].items; var flat = grouped ? list.reduce(function (a, g) { return a.concat(g.items || []); }, []) : list; var ids = flat.map(function (o) { return o.id; }); // Optional legacy spectrum: marks several catalog ids with localized // tags and notes instead of one recommendation badge. var spectrum = (opts2.spectrum && opts2.spectrum.length) ? opts2.spectrum : null; var specById = {}; if (spectrum) spectrum.forEach(function (s) { if (s && s.id) specById[s.id] = { tag: localized(s, "tag"), note: localized(s, "note") }; }); var allowCustom = opts2.allowCustom === true; // only for fields not fully enumerable var aiCustom = opts2.aiCustom || null; var customSentinel = opts2.customSentinel || (aiCustom ? "custom" : ""); var customInvalidValues = opts2.customInvalidValues || []; var cur = getVal(); var isCustom = cur != null && cur !== "" && (cur === customSentinel || ids.indexOf(cur) === -1); if (!allowCustom && isCustom && opts2.preserveCustom !== true) { // closed field with an out-of-catalog value → snap to recommended/first cur = ids.indexOf(recommendedId) >= 0 ? recommendedId : ids[0]; setVal(cur); isCustom = false; } var allChips = []; var customInput = el(aiCustom ? "textarea" : "input", "text-input custom-input"); setNaturalInputDirection(customInput); if (opts2.inputClass) customInput.classList.add(opts2.inputClass); if (aiCustom) customInput.rows = aiCustom.rows || 4; else customInput.type = "text"; customInput.placeholder = opts2.placeholder || t("custom_placeholder"); customInput.style.display = "none"; var customPreview = null; function customText() { if (!aiCustom) return customInput.value || ""; return String(aiCustom.getText ? aiCustom.getText() : (aiCustom.text || "")); } function syncAiCustom(selected) { if (!aiCustom || !customPreview) return; var value = customText(); customPreview.textContent = value; customPreview.style.display = selected ? "none" : "block"; customInput.style.display = selected ? "block" : "none"; if (selected && customInput.value !== value) customInput.value = value; } function deselect() { allChips.forEach(function (c) { c.classList.remove("selected"); }); if (aiCustom) syncAiCustom(false); } function makeChip(o) { var label = optionLabel(o); var desc = optionDesc(o); var spec = specById[o.id]; var chip = el("div", "chip"); var preview = previewNode(opts2.preview, o.id); if (preview) { chip.classList.add("chip-with-preview"); chip.classList.add("chip-preview-" + opts2.preview); chip.appendChild(preview); } var copy = el("div", "chip-copy"); if (o.viewbox) { label = label + (o.dim ? " · " + o.dim : ""); } else { if (o.dim) label += " · " + o.dim; if (desc) label += (LANG === "zh" || LANG === "ja" ? ":" : " — ") + desc; if (spec && spec.note) label += " · " + spec.note; } copy.appendChild(el("span", "chip-text", label)); if (spec) { // spectrum pick: badge shows its temperament tag, not the generic ★ chip.classList.add("recommended"); copy.appendChild(el("span", "rec-badge", "★ " + (spec.tag || t("recommended")))); } else if (!spectrum && o.id === recommendedId) { chip.classList.add("recommended"); copy.appendChild(el("span", "rec-badge", "★ " + t("recommended"))); } chip.appendChild(copy); if (!isCustom && o.id === cur) chip.classList.add("selected"); chip.addEventListener("click", function () { deselect(); chip.classList.add("selected"); if (!aiCustom) customInput.style.display = "none"; setVal(o.id); }); allChips.push(chip); return chip; } var chipsClass = "chips" + (opts2.chipsClass ? " " + opts2.chipsClass : ""); if (grouped) { list.forEach(function (g) { if (groupLabel(g)) parent.appendChild(el("div", "group-label", groupLabel(g))); var row = el("div", chipsClass); (g.items || []).forEach(function (o) { row.appendChild(makeChip(o)); }); parent.appendChild(row); }); if (allowCustom) { var lastRow = el("div", aiCustom ? "chips custom-chip-row" : "chips"); lastRow.appendChild(buildCustomChip()); parent.appendChild(lastRow); } } else { var wrap = el("div", chipsClass); var ownRowIds = opts2.ownRowIds || []; flat.filter(function (o) { return ownRowIds.indexOf(o.id) === -1; }) .forEach(function (o) { wrap.appendChild(makeChip(o)); }); if (allowCustom && !opts2.customOnOwnRow) wrap.appendChild(buildCustomChip()); parent.appendChild(wrap); flat.filter(function (o) { return ownRowIds.indexOf(o.id) >= 0; }) .forEach(function (o) { var ownRow = el("div", "chips standalone-chip-row"); ownRow.appendChild(makeChip(o)); parent.appendChild(ownRow); }); if (allowCustom && opts2.customOnOwnRow) { var customRow = el("div", "chips custom-chip-row"); customRow.appendChild(buildCustomChip()); parent.appendChild(customRow); } } if (allowCustom && !aiCustom) parent.appendChild(customInput); function buildCustomChip() { var customChip = el("div", aiCustom ? "chip ai-custom-candidate" : "chip"); if (aiCustom) { var customHead = el("div", "ai-custom-candidate-head"); customHead.appendChild(el("span", "chip-text", aiCustom.label || t("ai_custom_candidate"))); customHead.appendChild(el("span", "ai-custom-candidate-hint", aiCustom.hint || t("ai_custom_candidate_hint"))); customChip.appendChild(customHead); customPreview = el("div", "ai-custom-candidate-copy", customText()); customChip.appendChild(customPreview); customChip.appendChild(customInput); } else { customChip.appendChild(el("span", "chip-text", t("custom"))); } if (recommendedId === customSentinel || (recommendedId && ids.indexOf(recommendedId) === -1 && isCustom)) { customChip.classList.add("recommended"); customChip.appendChild(el("span", "rec-badge", "★ " + t("recommended"))); } if (isCustom) { customChip.classList.add("selected"); if (aiCustom) syncAiCustom(true); else { customInput.style.display = "block"; customInput.value = customInvalidValues.indexOf(cur) >= 0 ? "" : cur; } } else if (aiCustom) { syncAiCustom(false); } customChip.addEventListener("click", function () { deselect(); customChip.classList.add("selected"); if (aiCustom) syncAiCustom(true); else customInput.style.display = "block"; customInput.focus(); setVal(aiCustom ? customSentinel : (customInput.value || customSentinel)); }); allChips.push(customChip); return customChip; } customInput.addEventListener("click", function (event) { event.stopPropagation(); }); customInput.addEventListener("input", function () { if (aiCustom) { if (aiCustom.setText) aiCustom.setText(customInput.value); setVal(customSentinel); } else { setVal(customInput.value || customSentinel); } }); } function textField(parent, getVal, setVal, placeholderKey, numeric) { var input = el("input", numeric ? "num-input" : "text-input"); input.type = "text"; if (!numeric) setNaturalInputDirection(input); input.value = getVal() || ""; input.placeholder = t(placeholderKey); input.addEventListener("input", function () { setVal(input.value); }); parent.appendChild(input); } function textareaField(parent, getVal, setVal, placeholderKey, rows) { var input = el("textarea", "text-input"); setNaturalInputDirection(input); input.rows = rows || 2; input.value = getVal() || ""; input.placeholder = t(placeholderKey); input.addEventListener("input", function () { setVal(input.value); }); parent.appendChild(input); return input; } function labeledTextarea(parent, labelKey, getVal, setVal, placeholderKey, hintKey, rows) { var field = el("div", "subfield"); field.appendChild(el("div", "subfield-label", t(labelKey))); if (hintKey) field.appendChild(el("div", "toggle-desc", t(hintKey))); textareaField(field, getVal, setVal, placeholderKey, rows); parent.appendChild(field); return field; } function normPalette(c) { function read(src, keys) { if (!src) return undefined; for (var i = 0; i < keys.length; i += 1) { if (src[keys[i]] != null) return src[keys[i]]; } return undefined; } function collect(src) { return { background: read(src, ["background", "bg"]), secondary_bg: read(src, ["secondary_bg", "secondary_background", "card_bg", "card_background"]), primary: read(src, ["primary"]), accent: read(src, ["accent"]), secondary_accent: read(src, ["secondary_accent", "secondary"]), body_text: read(src, ["body_text", "text"]) }; } if (c && c.palette) { return collect(c.palette); } if (!c) return {}; return collect(c); } function recommendationLanguage() { // `lang` controls the Confirm UI language; it is not evidence of the // deck's content language. New staged files carry `primary_language`. var raw = REC && (REC.primary_language || REC.content_language || REC.language); if (raw && typeof raw === "object") { raw = raw.value || raw.id || raw.code || ""; } // The API is the single normalizer; the browser consumes its canonical // value instead of maintaining a second alias table. return String(raw || "und").trim().replace(/_/g, "-"); } function isEnglishProject() { return /^en(?:-|$)/i.test(recommendationLanguage()); } function setEnglishLanguageAttributes(node) { node.lang = "en"; node.dir = "ltr"; } function setUiLanguageAttributes(node) { node.lang = LANG === "zh" ? "zh-CN" : (LANG === "zh-TW" ? "zh-TW" : (LANG === "ja" ? "ja-JP" : "en-US")); node.dir = "ltr"; } function setNaturalInputDirection(node) { node.dir = "auto"; } function stringList(value) { if (Array.isArray(value)) return value.map(String); if (value == null || value === "") return []; return [String(value)]; } function normalizedFontToken(value) { return String(value || "").trim().toLowerCase().replace(/[_\s]+/g, "-"); } function fontScriptTokens(language) { var parts = normalizedFontToken(language).split("-"); var base = parts[0]; if (!base || base === "und") return []; var explicitScript = parts.filter(function (part) { return /^[a-z]{4}$/.test(part); })[0]; if (explicitScript) return [explicitScript]; if (base === "en") return ["latin", "latn", "english"]; if (base === "zh") { if (parts.indexOf("cn") >= 0 || parts.indexOf("sg") >= 0) { return ["han", "hans", "cjk", "chinese", "zh"]; } if (parts.indexOf("tw") >= 0 || parts.indexOf("hk") >= 0 || parts.indexOf("mo") >= 0) { return ["han", "hant", "cjk", "chinese", "zh"]; } return ["han", "hans", "cjk", "chinese", "zh"]; } if (base === "ja") return ["han", "jpan", "kana", "hiragana", "katakana", "cjk", "japanese", "ja"]; if (base === "ko") return ["hangul", "kore", "han", "cjk", "korean", "ko"]; if (["ru", "uk", "bg", "sr", "mk"].indexOf(base) >= 0) return ["cyrl", "cyrillic"]; if (["ar", "fa", "ur"].indexOf(base) >= 0) return ["arab", "arabic"]; if (base === "he" || base === "iw") return ["hebr", "hebrew"]; if (base === "el") return ["grek", "greek"]; if (base === "th") return ["thai"]; if (base === "hi" || base === "mr" || base === "ne") return ["deva", "devanagari"]; // Most remaining presentation locales use Latin script. Locale metadata // still wins before this fallback. return ["latin", "latn"]; } function fontSupportsLanguage(font, language) { var target = normalizedFontToken(language); var targetBase = target.split("-")[0]; var locales = stringList(font && font.locales).map(normalizedFontToken); if (locales.some(function (locale) { return locale === "*" || locale === target || (target !== targetBase && locale === targetBase); })) return true; var supportedScripts = stringList(font && font.scripts).map(normalizedFontToken); var wantedScripts = fontScriptTokens(language); if (supportedScripts.some(function (script) { return wantedScripts.indexOf(script) >= 0; })) return true; // A catalog entry without language metadata remains usable. Once either // metadata field is present, filtering follows it. return !locales.length && !supportedScripts.length; } function fontCatalogEntries(field) { var fonts = (CAT && Array.isArray(CAT.fonts)) ? CAT.fonts : []; var language = field === "english" ? "en" : recommendationLanguage(); if (field === "primary" && language === "und") { return fonts.filter(function (font) { return font && font.id; }); } var matching = fonts.filter(function (font) { return font && font.id && fontSupportsLanguage(font, language); }); return matching; } function findFontCatalogEntry(id) { var fonts = (CAT && Array.isArray(CAT.fonts)) ? CAT.fonts : []; var target = String(id || "").trim().toLowerCase(); for (var i = 0; i < fonts.length; i += 1) { if (String(fonts[i].id || "").trim().toLowerCase() === target) return fonts[i]; } return null; } function derivedFontCss(font) { var primary = findFontCatalogEntry(font && font.primary); return String((primary && primary.css) || "sans-serif").trim() || "sans-serif"; } function normalizedFontRole(font, samples) { font = (font && typeof font === "object") ? Object.assign({}, font) : {}; samples = samples || {}; var primary = font.primary; var english = font.english; if (isEnglishProject()) { primary = primary || font.latin || font.cjk || ""; english = ""; } else { primary = primary || font.cjk || ""; english = english || font.latin || ""; } font.primary = primary; if (isEnglishProject()) delete font.english; else font.english = english; font.sample_primary = font.sample_primary || (isEnglishProject() ? font.sample_latin : font.sample_cjk) || samples.primary || ""; if (isEnglishProject()) { delete font.sample_english; } else { font.sample_english = font.sample_english || font.sample_latin || samples.english || ""; } delete font.cjk; delete font.latin; delete font.sample_cjk; delete font.sample_latin; if (!String(font.css || "").trim()) font.css = derivedFontCss(font); return font; } function normTypography(c) { c = c || {}; var normalized = Object.assign({}, c); var objectShape = c.heading && typeof c.heading === "object" && c.body && typeof c.body === "object"; var heading = objectShape ? c.heading : { primary: isEnglishProject() ? (c.heading_latin || c.heading || "") : (c.heading || ""), english: isEnglishProject() ? "" : (c.heading_latin || ""), css: c.heading_css || "" }; var body = objectShape ? c.body : { primary: isEnglishProject() ? (c.body_latin || c.body || "") : (c.body || ""), english: isEnglishProject() ? "" : (c.body_latin || ""), css: c.body_css || "" }; normalized.name = c.name || ""; normalized.note = c.note || ""; normalized.custom = c.custom || ""; normalized.body_size = typographyBodySize(c); normalized.heading = normalizedFontRole(heading, { primary: isEnglishProject() ? (c.sample_heading_latin || c.sample_heading || "") : (c.sample_heading || ""), english: c.sample_heading_latin || "" }); normalized.body = normalizedFontRole(body, { primary: isEnglishProject() ? (c.sample_body_latin || c.sample_body || "") : (c.sample_body || ""), english: c.sample_body_latin || "" }); delete normalized.heading_latin; delete normalized.body_latin; delete normalized.heading_css; delete normalized.body_css; delete normalized.sample_heading; delete normalized.sample_heading_latin; delete normalized.sample_body; delete normalized.sample_body_latin; return normalized; } function typographyBodySize(c) { c = c || {}; var value = c.body_size || c.body_baseline || c.body_px || (c.sizes && c.sizes.body) || (c.size && c.size.body) || (c.body && typeof c.body === "object" && (c.body.size || c.body.font_size)); return value == null ? "" : String(value).replace(/px$/i, ""); } function designDirectionSpec() { return (REC && REC.design_directions) || (REC && REC.design && REC.design.directions) || {}; } function designDirectionCandidates() { var spec = designDirectionSpec(); return spec.candidates || spec.options || []; } function selectedDesignDirectionIndex() { var candidates = designDirectionCandidates(); var selected = Number(designDirectionSpec().selected); if (!isFinite(selected) || selected < 0) selected = 0; return Math.min(Math.floor(selected), Math.max(candidates.length - 1, 0)); } function designDirectionId(candidate, index) { var value = candidate && candidate.id; return String(value || ("direction-" + (Number(index) + 1))); } function selectedDesignDirection() { var candidates = designDirectionCandidates(); return candidates[selectedDesignDirectionIndex()] || {}; } function directionField(field) { var candidate = selectedDesignDirection(); return candidate[field] != null ? candidate[field] : null; } function directionBehavior(candidate, field) { return String(localized(candidate, field + "_behavior") || (candidate && candidate[field + "_behavior"]) || ""); } function directionStateSignature(candidate) { candidate = candidate || {}; var out = {}; if (candidate.mode) { out.mode = STATE.mode || ""; out.mode_behavior = STATE.mode === "custom" ? (STATE.mode_behavior || "") : ""; } if (candidate.visual_style) { out.visual_style = STATE.visual_style || ""; out.visual_style_behavior = STATE.visual_style === "custom" ? (STATE.visual_style_behavior || "") : ""; } if (candidate.color) { var palette = normPalette(STATE.color || {}); out.color = { custom: (STATE.color && STATE.color.custom) || "", palette: {} }; PALETTE_ROLES.forEach(function (role) { out.color.palette[role] = normHex(palette[role]) || ""; }); } if (candidate.typography) { var typography = normTypography(STATE.typography || {}); out.typography = { heading: typography.heading || {}, body: typography.body || {}, body_size: String(typography.body_size || ""), sizes: {} }; ["title", "subtitle", "annotation"].forEach(function (role) { out.typography.sizes[role] = String( (STATE.typography && STATE.typography.sizes && STATE.typography.sizes[role]) || "" ); }); } if (candidate.icons) out.icons = STATE.icons || ""; if (candidate.image_strategy) { out.image_strategy = comparableImageStrategy(STATE.image_strategy || {}); } return JSON.stringify(out); } function activeDirectionAdjusted(candidate, index) { return designDirectionId(candidate, index) === ACTIVE_DIRECTION_ID && directionStateSignature(candidate) !== ACTIVE_DIRECTION_BASELINE; } function customCandidateSpec(field) { var candidates = REC && REC.custom_candidates; var candidate = candidates && candidates[field]; if (typeof candidate === "string") return { behavior: candidate }; if (candidate && typeof candidate === "object") return candidate; var legacy = REC && REC[field + "_behavior"]; if (legacy && typeof legacy === "object") legacy = legacy.value; return legacy ? { behavior: legacy } : {}; } function customCandidateBehavior(field) { var candidate = customCandidateSpec(field); return String(localized(candidate, "behavior") || candidate.value || candidate.custom || ""); } function creativeCustomOptions(field, stateKey, placeholderKey) { var candidate = customCandidateSpec(field); var current = String(STATE[stateKey] || customCandidateBehavior(field) || ""); if (!current.trim()) return null; STATE[stateKey] = current; return { label: localized(candidate, "name") || t("ai_custom_candidate"), hint: t("ai_custom_candidate_hint"), getText: function () { return STATE[stateKey] || ""; }, setText: function (value) { STATE[stateKey] = value; }, rows: 4, placeholder: t(placeholderKey) }; } function colorRecommendationCandidates() { var direct = (REC.color && REC.color.candidates) || []; if (direct.length) return direct; return designDirectionCandidates().map(function (candidate) { return candidate && candidate.color; }).filter(Boolean); } function typographyRecommendationCandidates() { var direct = (REC.typography && REC.typography.candidates) || []; if (direct.length) return direct; return designDirectionCandidates().map(function (candidate) { return candidate && candidate.typography; }).filter(Boolean); } function imageStrategySpec() { return (REC && REC.image_strategy) || (REC && REC.images && REC.images.strategy) || (REC && REC.images && REC.images.ai_strategy) || {}; } function imageStrategyCandidates() { var spec = imageStrategySpec(); var direct = spec.candidates || spec.options || []; if (direct.length) return direct; return designDirectionCandidates().map(function (candidate) { return candidate && candidate.image_strategy; }).filter(Boolean); } function imageStrategyRecommendationCandidates() { return imageStrategyCandidates().filter(function (candidate) { return candidate && candidate.rendering !== "custom"; }).slice(0, 3); } function imageStrategyCatalogCandidates() { var items = AI_IMAGE_COMPARISON && AI_IMAGE_COMPARISON.rendering; if (!Array.isArray(items)) return []; return items.map(function (item) { return item && item.id ? { rendering: item.id } : null; }).filter(Boolean); } function imageStrategySelectableCandidates() { var recommended = imageStrategyRecommendationCandidates(); var seen = {}; recommended.forEach(function (candidate) { seen[candidate.rendering] = true; }); return recommended.concat(imageStrategyCatalogCandidates().filter(function (candidate) { return !seen[candidate.rendering]; })); } function imageStrategyCustomCandidate() { var candidate = customCandidateSpec("image_strategy"); if (!customCandidateBehavior("image_strategy")) return null; if (!candidate || typeof candidate !== "object") candidate = {}; candidate = Object.assign({}, candidate, { rendering: "custom" }); return normalizedImageStrategy(candidate); } function normalizedImageStrategy(candidate) { candidate = candidate || {}; var out = { name: localized(candidate, "name") || candidate.name || "", rendering: candidate.rendering || "", visual: localized(candidate, "visual") || "", mood: localized(candidate, "mood") || "" }; var behavior = localized(candidate, "behavior") || candidate.behavior || candidate.custom || ""; if (behavior) out.behavior = behavior; return out; } function comparableImageStrategy(candidate) { var strategy = normalizedImageStrategy(candidate); return { rendering: strategy.rendering || "", visual: strategy.visual || "", mood: strategy.mood || "", behavior: strategy.behavior || "" }; } function directionCardSummary(candidate) { candidate = candidate || {}; var strategy = normalizedImageStrategy(candidate.image_strategy || {}); return String( localized(candidate, "note") || directionBehavior(candidate, "visual_style") || directionBehavior(candidate, "mode") || strategy.visual || strategy.behavior || "" ); } function usesCustomImagePlanValue(value) { var ids = (CAT.image_usage || []).map(function (item) { return item.id; }); if (Array.isArray(value)) return false; return value && ids.indexOf(value) === -1; } function customImagePlanHasAiSignal() { return imageStrategyRecommendationCandidates().length > 0 || !!recId("image_ai_path"); } function needsGeneratedImagesForUsage(value) { if (Array.isArray(value)) return value.indexOf("ai") >= 0; return value === "ai" || (usesCustomImagePlanValue(value) && customImagePlanHasAiSignal()); } function selectedImageUsageIds(value) { var validIds = (CAT.image_usage || []).map(function (item) { return item.id; }); return normalizeRecIds("image_usage", value).filter(function (id) { return validIds.indexOf(id) >= 0; }); } function imageUsageNotesRecommendation(rawUsage) { var notes = (REC && REC.image_notes && REC.image_notes.value) || (REC && REC.image_notes) || (REC && REC.images && REC.images.notes) || ""; if (!notes && usesCustomImagePlanValue(rawUsage)) notes = rawUsage; return typeof notes === "string" ? notes : ""; } function defaultImageUsageId() { return firstId(CAT.image_usage); } function imageStrategySelectedIndex() { var spec = imageStrategySpec(); var direct = spec.candidates || spec.options || []; var idx = direct.length ? Number(spec.selected || 0) : selectedDesignDirectionIndex(); if (!isFinite(idx) || idx < 0) idx = 0; return Math.min( Math.floor(idx), Math.max(imageStrategyRecommendationCandidates().length - 1, 0) ); } // ---- section renderers ---------------------------------------------- function renderCanvas(host) { var sec = section(1, "sec_canvas"); enumField(sec, CAT.canvas, recOrFirst("canvas", CAT.canvas), function () { return STATE.canvas; }, function (v) { STATE.canvas = v; if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; // Canvas changes dimensions only — never silently rewrite font sizes // the user can see / edit. The size hint re-renders with the new // canvas; a default body is filled only when none is set yet. if (!STATE.typography.body_size) { STATE.typography.body_size = defaultBodySizeForCanvas(v, STATE.delivery_purpose); } renderAll(); }, { allowCustom: true }); host.appendChild(sec); } function renderPages(host) { var sec = section(2, "sec_pages"); textField(sec, function () { return STATE.page_count; }, function (v) { STATE.page_count = v; }, "placeholder_pages", true); host.appendChild(sec); } function templateApplicationRecommendation() { if (!REC || REC.template_application == null) return null; var field = REC.template_application; if (typeof field === "object") { return field.value == null ? "" : String(field.value); } return String(field); } function renderTemplateApplication(host) { if (templateApplicationRecommendation() == null) return; var sec = section("T", "sec_template_application"); setSectionNote(sec, t("template_application_hint")); textareaField(sec, function () { return STATE.template_application; }, function (v) { STATE.template_application = v; }, "placeholder_template_application", 4); host.appendChild(sec); } function renderCommunication(host) { var sec = section(1, "sec_communication"); var audienceField = el("div", "subfield"); audienceField.appendChild(el("div", "subfield-label", t("sec_audience"))); textField(audienceField, function () { return STATE.audience; }, function (v) { STATE.audience = v; }, "placeholder_audience", false); sec.appendChild(audienceField); labeledTextarea(sec, "communication_intent", function () { return STATE.communication_intent; }, function (v) { STATE.communication_intent = v; }, "placeholder_communication_intent", "communication_intent_hint", 3); labeledTextarea(sec, "audience_outcome", function () { return STATE.audience_outcome; }, function (v) { STATE.audience_outcome = v; }, "placeholder_audience_outcome", null, 2); labeledTextarea(sec, "core_message", function () { return STATE.core_message; }, function (v) { STATE.core_message = v; }, "placeholder_core_message", null, 2); host.appendChild(sec); } function renderDelivery(host) { var sec = section(2, "sec_delivery"); labeledTextarea(sec, "delivery_context", function () { return STATE.delivery_context; }, function (v) { STATE.delivery_context = v; }, "placeholder_delivery_context", "delivery_context_hint", 2); labeledTextarea(sec, "artifact_afterlife", function () { return STATE.artifact_afterlife; }, function (v) { STATE.artifact_afterlife = v; }, "placeholder_artifact_afterlife", null, 2); // Material divergence remains open prose: it controls how source material // may be reshaped, independently of communication intent and template reuse. var divergenceField = labeledTextarea(sec, "sub_divergence", function () { return STATE.content_divergence; }, function (v) { STATE.content_divergence = v; }, "placeholder_divergence", null, 2); if (recommendationFieldLocked("content_divergence")) { var divergenceInput = divergenceField.querySelector("textarea"); if (divergenceInput) { divergenceInput.readOnly = true; divergenceInput.classList.add("locked-field"); } divergenceField.appendChild(el("div", "toggle-desc locked-field-hint", t("content_divergence_locked_hint"))); } host.appendChild(sec); } function renderReadingMode(host) { if (!isPptCanvas(STATE.canvas)) return; var sec = section("D", "delivery_purpose"); setSectionNote(sec, t("delivery_purpose_hint")); enumField(sec, CAT.delivery_purpose, recOrFirst("delivery_purpose", CAT.delivery_purpose), function () { return STATE.delivery_purpose; }, function (v) { STATE.delivery_purpose = v; // Same-stage dependency, resolved entirely in the browser: do // not ask the backend to author Stage 2 again. Manual size // overrides remain authoritative. syncUnpinnedTypographySizes(true); }); host.appendChild(sec); } function applyDesignDirection(candidate, index, shouldRender) { candidate = candidate || {}; var directionId = designDirectionId(candidate, index); if (candidate.mode) { STATE.mode = candidate.mode; STATE.mode_behavior = candidate.mode === "custom" ? directionBehavior(candidate, "mode") : ""; ACTIVE_COMPONENT_DIRECTION_IDS.mode = directionId; } if (candidate.visual_style) { STATE.visual_style = candidate.visual_style; STATE.visual_style_behavior = candidate.visual_style === "custom" ? directionBehavior(candidate, "visual_style") : ""; ACTIVE_COMPONENT_DIRECTION_IDS.visual_style = directionId; } if (candidate.color) { STATE.color = { name: localized(candidate.color, "name") || candidate.color.name || "", palette: Object.assign({}, normPalette(candidate.color)) }; } if (candidate.typography) { var typography = normTypography(candidate.typography); resetTypographySizeOverrides(); STATE.typography = typography; STATE.typography.name = localized(candidate.typography, "name") || typography.name || ""; STATE.typography.body_size = typography.body_size || defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose); STATE.typography.sizes = Object.assign({}, typography.sizes || {}); syncUnpinnedTypographySizes(false); } if (candidate.icons) STATE.icons = normalizeRecId("icons", candidate.icons); if (candidate.image_strategy) { STATE.image_strategy = normalizedImageStrategy(candidate.image_strategy); ACTIVE_COMPONENT_DIRECTION_IDS.image_strategy = directionId; } ACTIVE_DIRECTION_ID = directionId; ACTIVE_DIRECTION_BASELINE = directionStateSignature(candidate); if (shouldRender !== false) renderAll(); } function renderDesignDirections(host) { var candidates = designDirectionCandidates(); if (!candidates.length) return; var sec = section("B", "sec_design_directions", t("design_directions_hint")); var grid = el("div", "font-grid design-direction-grid"); var cardStates = []; var recommendedIndex = selectedDesignDirectionIndex(); candidates.forEach(function (candidate, idx) { var card = el("div", "font-card design-direction-card"); card.title = t("direction_apply_hint"); var head = el("div", "font-card-head"); head.appendChild(el("span", "font-card-name", localized(candidate, "name") || (t("option_prefix") + " " + (idx + 1)))); if (idx === recommendedIndex) { head.appendChild(el("span", "rec-badge", "★ " + t("recommended"))); } var status = el("span", "rec-badge direction-status"); status.style.display = "none"; head.appendChild(status); card.appendChild(head); var customVisual = candidate.visual_style === "custom"; if (candidate.visual_style) { var preview = el("div", "design-direction-preview"); if (customVisual) { preview.classList.add("design-direction-custom-preview"); preview.appendChild( el("div", "design-direction-custom-label", t("custom")) ); preview.appendChild(el( "div", "design-direction-custom-copy", directionCardSummary(candidate) || t("custom") )); } else { appendVisualStyleImage(preview, candidate.visual_style); } card.appendChild(preview); } var meta = []; if (candidate.mode && candidate.mode !== "custom") { meta.push(directionComponentValueLabel(candidate, "mode")); } if (candidate.visual_style && !customVisual) { meta.push(directionComponentValueLabel(candidate, "visual_style")); } var typographyName = candidate.typography && (localized(candidate.typography, "name") || candidate.typography.name); if (typographyName) meta.push(typographyName); if (candidate.icons) meta.push(humanizeId(candidate.icons)); if (candidate.image_strategy && candidate.image_strategy.rendering && candidate.image_strategy.rendering !== "custom") { meta.push(comparisonValueLabel("rendering", candidate.image_strategy.rendering)); } if (meta.length) card.appendChild(el("div", "font-card-meta", meta.join(" · "))); var palette = normPalette(candidate.color || {}); var swatches = el("div", "palette-swatches design-direction-swatches"); PALETTE_ROLES.forEach(function (role) { var value = normHex(palette[role]); if (!value) return; var swatch = el("span", "swatch"); swatch.style.background = value; swatch.title = role + ": " + value; swatches.appendChild(swatch); }); if (swatches.childElementCount) card.appendChild(swatches); var note = localized(candidate, "note"); if (note && !customVisual) card.appendChild(el("div", "color-note", note)); var restore = el("button", "direction-reset-button", t("direction_restore")); restore.type = "button"; restore.hidden = true; restore.addEventListener("click", function (event) { event.stopPropagation(); applyDesignDirection(candidate, idx); }); card.appendChild(restore); card.addEventListener("click", function () { if (designDirectionId(candidate, idx) === ACTIVE_DIRECTION_ID) return; applyDesignDirection(candidate, idx); }); cardStates.push({ candidate: candidate, index: idx, card: card, status: status, restore: restore }); grid.appendChild(card); }); refreshDesignDirectionState = function () { cardStates.forEach(function (entry) { var active = designDirectionId(entry.candidate, entry.index) === ACTIVE_DIRECTION_ID; var adjusted = active && activeDirectionAdjusted(entry.candidate, entry.index); entry.card.classList.toggle("selected", active && !adjusted); entry.card.classList.toggle("adjusted", adjusted); entry.status.style.display = active ? "inline-block" : "none"; entry.status.textContent = adjusted ? t("direction_adjusted") : t("direction_active"); entry.restore.hidden = !(active && adjusted); entry.card.title = active ? "" : t("direction_apply_hint"); }); }; refreshDesignDirectionState(); sec.appendChild(grid); host.appendChild(sec); } function directionComponentMatchesAuthored(candidate, field) { if (field === "mode") { return STATE.mode === candidate.mode && (candidate.mode !== "custom" || String(STATE.mode_behavior || "") === directionBehavior(candidate, "mode")); } if (field === "visual_style") { return STATE.visual_style === candidate.visual_style && (candidate.visual_style !== "custom" || String(STATE.visual_style_behavior || "") === directionBehavior(candidate, "visual_style")); } if (field === "image_strategy") { return JSON.stringify(comparableImageStrategy(STATE.image_strategy || {})) === JSON.stringify(comparableImageStrategy(candidate.image_strategy || {})); } return false; } function directionComponentSelected(candidate, field, index) { var activeId = ACTIVE_COMPONENT_DIRECTION_IDS[field]; if (activeId) return activeId === designDirectionId(candidate, index); return directionComponentMatchesAuthored(candidate, field); } function applyDirectionComponent(candidate, field, index) { if (field === "mode") { STATE.mode = candidate.mode; STATE.mode_behavior = candidate.mode === "custom" ? directionBehavior(candidate, "mode") : ""; } else if (field === "visual_style") { STATE.visual_style = candidate.visual_style; STATE.visual_style_behavior = candidate.visual_style === "custom" ? directionBehavior(candidate, "visual_style") : ""; } else if (field === "image_strategy") { STATE.image_strategy = normalizedImageStrategy(candidate.image_strategy || {}); } ACTIVE_COMPONENT_DIRECTION_IDS[field] = designDirectionId(candidate, index); renderAll(); } function directionComponentIsCustom(candidate, field) { if (field === "image_strategy") { return candidate && candidate.image_strategy && candidate.image_strategy.rendering === "custom"; } return candidate && candidate[field] === "custom"; } function setDirectionComponentBehavior(field, value) { if (field === "mode") { STATE.mode = "custom"; STATE.mode_behavior = value; } else if (field === "visual_style") { STATE.visual_style = "custom"; STATE.visual_style_behavior = value; } else if (field === "image_strategy") { STATE.image_strategy = normalizedImageStrategy(STATE.image_strategy || {}); STATE.image_strategy.rendering = "custom"; STATE.image_strategy.behavior = value; } } function directionComponentValueLabel(candidate, field) { var value = candidate && candidate[field]; if (field === "image_strategy") { value = value && value.rendering; return comparisonValueLabel("rendering", value); } var catalog = field === "mode" ? CAT.modes : CAT.visual_styles; var option = findCatalogOption(catalog, value); return option ? optionLabel(option) : (value === "custom" ? t("custom") : humanizeId(value)); } function directionComponentNote(candidate, field) { if (field === "image_strategy") { var strategy = normalizedImageStrategy(candidate.image_strategy || {}); return [strategy.visual, strategy.mood, strategy.behavior].filter(Boolean).join(" · "); } if (candidate[field] === "custom") return directionBehavior(candidate, field); return localized(candidate, "note") || ""; } function directionComponentEditableBehavior(candidate, field) { if (field === "image_strategy") { return normalizedImageStrategy(candidate.image_strategy || {}).behavior || ""; } return directionBehavior(candidate, field); } function renderDirectionComponentCandidates(parent, field) { var candidates = designDirectionCandidates().filter(function (candidate) { return candidate && candidate[field]; }); if (!candidates.length) return; var block = el("div", "subfield scheme-component-options"); block.appendChild(el("div", "subfield-label", t("scheme_component_options"))); var grid = el("div", "font-grid scheme-component-grid"); var entries = []; candidates.forEach(function (candidate, index) { var card = el("div", "font-card scheme-component-card"); card.dataset.candidateId = designDirectionId(candidate, index) + ":" + field; var head = el("div", "font-card-head"); head.appendChild(el("span", "font-card-name", localized(candidate, "name") || (t("option_prefix") + " " + (index + 1)))); head.appendChild(el("span", "font-card-meta", directionComponentValueLabel(candidate, field))); card.appendChild(head); if (field === "visual_style" && candidate.visual_style !== "custom") { var preview = el("div", "design-direction-preview"); appendVisualStyleImage(preview, candidate.visual_style); card.appendChild(preview); } var note = directionComponentNote(candidate, field); var noteNode = note ? el("div", "color-note", note) : null; if (noteNode) card.appendChild(noteNode); var editor = null; if (directionComponentIsCustom(candidate, field)) { editor = el("textarea", "text-input scheme-component-editor"); setNaturalInputDirection(editor); editor.rows = 4; editor.value = directionComponentEditableBehavior(candidate, field); editor.placeholder = t(field === "mode" ? "mode_behavior_placeholder" : (field === "visual_style" ? "visual_style_behavior_placeholder" : "image_strategy_custom_placeholder")); editor.style.display = "none"; editor.addEventListener("click", function (event) { event.stopPropagation(); }); editor.addEventListener("input", function () { setDirectionComponentBehavior(field, editor.value); if (field === "image_strategy") refreshImageStrategyPreview(); refreshDesignDirectionState(); refreshDirectionComponentStates(); }); card.appendChild(editor); } card.addEventListener("click", function () { applyDirectionComponent(candidate, field, index); }); entries.push({ candidate: candidate, index: index, card: card, note: noteNode, editor: editor }); grid.appendChild(card); }); var paint = function () { entries.forEach(function (entry) { var selected = directionComponentSelected( entry.candidate, field, entry.index ); var adjusted = selected && !directionComponentMatchesAuthored(entry.candidate, field); entry.card.classList.toggle("selected", selected); entry.card.classList.toggle("adjusted", adjusted); if (entry.editor) { entry.editor.style.display = selected ? "block" : "none"; if (selected && document.activeElement !== entry.editor) { var current = field === "mode" ? STATE.mode_behavior : (field === "visual_style" ? STATE.visual_style_behavior : ((STATE.image_strategy || {}).behavior || "")); entry.editor.value = current; } if (selected) fitTextareaToContent(entry.editor); } if (entry.note) entry.note.style.display = selected ? "none" : "block"; }); }; DIRECTION_COMPONENT_PAINTERS.push(paint); paint(); block.appendChild(grid); parent.appendChild(block); } function currentDirectionCustomCandidate(field, stateKey) { if (STATE[field] !== "custom") return null; return designDirectionCandidates().filter(function (candidate) { return candidate && candidate[field] === "custom" && directionBehavior(candidate, field) === String(STATE[stateKey] || ""); })[0]; } function renderCurrentDirectionCustomEditor(parent, field, stateKey, placeholderKey) { if (STATE[field] !== "custom") return null; if (ACTIVE_COMPONENT_DIRECTION_IDS[field]) return null; var source = currentDirectionCustomCandidate(field, stateKey); if (!source && customCandidateBehavior(field)) return null; var block = el("div", "subfield direction-custom-editor"); var label = t("custom"); if (source && localized(source, "name")) { label = localized(source, "name") + " · " + label; } block.appendChild(el("div", "subfield-label", label)); var input = el("textarea", "text-input custom-input"); setNaturalInputDirection(input); input.rows = 4; input.placeholder = t(placeholderKey); input.value = STATE[stateKey] || ""; input.style.display = "block"; input.addEventListener("input", function () { STATE[stateKey] = input.value; refreshDesignDirectionState(); refreshDirectionComponentStates(); }); block.appendChild(input); parent.appendChild(block); return block; } function renderNarrativeDirection(host) { var sec = section(4, "sec_narrative"); renderDirectionComponentCandidates(sec, "mode"); var directionCustomEditor = null; var custom = currentDirectionCustomCandidate("mode", "mode_behavior") ? null : creativeCustomOptions("mode", "mode_behavior", "mode_behavior_placeholder"); enumField(sec, CAT.modes, recOrFirst("mode", CAT.modes), function () { return STATE.mode; }, function (v) { STATE.mode = v; ACTIVE_COMPONENT_DIRECTION_IDS.mode = ""; if (v !== "custom") STATE.mode_behavior = ""; refreshDesignDirectionState(); refreshDirectionComponentStates(); if (directionCustomEditor) { directionCustomEditor.style.display = v === "custom" ? "block" : "none"; } }, { allowCustom: !!custom, customOnOwnRow: true, customSentinel: "custom", preserveCustom: true, placeholder: t("mode_behavior_placeholder"), aiCustom: custom }); directionCustomEditor = renderCurrentDirectionCustomEditor( sec, "mode", "mode_behavior", "mode_behavior_placeholder" ); host.appendChild(sec); } function renderVisualDirection(host) { var sec = section(5, "sec_visual"); renderDirectionComponentCandidates(sec, "visual_style"); var directionCustomEditor = null; var custom = currentDirectionCustomCandidate( "visual_style", "visual_style_behavior" ) ? null : creativeCustomOptions( "visual_style", "visual_style_behavior", "visual_style_behavior_placeholder" ); enumField(sec, CAT.visual_styles, recOrFirst("visual_style", CAT.visual_styles), function () { return STATE.visual_style; }, function (v) { STATE.visual_style = v; ACTIVE_COMPONENT_DIRECTION_IDS.visual_style = ""; if (v !== "custom") STATE.visual_style_behavior = ""; refreshDesignDirectionState(); refreshDirectionComponentStates(); if (directionCustomEditor) { directionCustomEditor.style.display = v === "custom" ? "block" : "none"; } }, { allowCustom: !!custom, customOnOwnRow: true, customSentinel: "custom", preserveCustom: true, placeholder: t("visual_style_behavior_placeholder"), aiCustom: custom, preview: "visual_style", chipsClass: "visual-style-grid" }); directionCustomEditor = renderCurrentDirectionCustomEditor( sec, "visual_style", "visual_style_behavior", "visual_style_behavior_placeholder" ); host.appendChild(sec); } var PALETTE_ROLES = [ "background", "secondary_bg", "primary", "accent", "secondary_accent", "body_text" ]; function normHex(val) { var v = (val || "").trim(); if (!/^#?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(v)) return null; return v.charAt(0) === "#" ? v : "#" + v; } function hexOr(val, fallback) { return normHex(val) || fallback; } // Replaced when the combined color+typography preview mounts; the color and // typography sections call it after every change so the preview stays live. var refreshStylePreview = function () {}; // Replaced when the selected generated-image preview mounts. var refreshImageStrategyPreview = function () {}; // Replaced when the final plan's image-production section mounts; image-use // edits call it so the conditional AI path stays synchronized on the page. var refreshImageProduction = function () {}; // Replaced when the typography section mounts; the canvas section calls it so // the body-size hint tracks the chosen canvas dimensions. var refreshBodySizeHint = function () {}; // Replaced when the typography section mounts; body-size / reading-mode // changes call it so unpinned per-role values update locally. var refreshSizeInputs = function () {}; // Per-role size slots the user can edit directly (parallel to color roles). // Defaults derive from `body` via mid-band ramp ratios (strategist.md §g); // values are px (the system's only unit). var SIZE_ROLES = ["title", "subtitle", "annotation"]; var SIZE_RATIO = { title: 1.75, subtitle: 1.35, annotation: 0.78 }; var TYPOGRAPHY_SIZE_OVERRIDES = { body: false, title: false, subtitle: false, annotation: false }; function resetTypographySizeOverrides() { Object.keys(TYPOGRAPHY_SIZE_OVERRIDES).forEach(function (role) { TYPOGRAPHY_SIZE_OVERRIDES[role] = false; }); } function deriveSize(role, bodyVal) { var raw = (bodyVal || 0) * (SIZE_RATIO[role] || 1); // All px. On PPT, snap the recommended role size to a clean even number so // the user sees conventional sizes (body 24 → title 42, subtitle 32), not // ratio leftovers. Non-PPT keeps a plain integer — large px, snapping moot. if (isPptCanvas(STATE.canvas)) return Math.round(raw / 2) * 2; return Math.round(raw); } // Canvas dimensions (viewBox user units) parsed from a catalog `dim` like // "1242×1660" or from a custom canvas string containing WxH; null if unknown. function canvasDimensions(canvasVal) { var dim = null; (CAT.canvas || []).forEach(function (o) { if (o.id === canvasVal) dim = o.dim; }); var m = String(dim || canvasVal || "").match(/(\d{2,5})\s*[×xX*]\s*(\d{2,5})/); return m ? { width: parseInt(m[1], 10), height: parseInt(m[2], 10) } : null; } // PPT canvases (16:9 / 4:3) take the fixed per-reading-mode body px; // other canvases use the canvas-owned effective-span rule below. function isPptCanvas(canvasVal) { var id = String(canvasVal || "").toLowerCase(); return id === "ppt169" || id === "ppt43"; } // Body baseline in **px** per reading mode (legacy key: // delivery_purpose). The system is px-only — these mirror the registered-PPT // values in references/canvas-formats.md. No pt layer, no conversion. `def` is the fixed // recommendation; lo/hi are a sanity envelope for the out-of-range flag only. function deliveryBodyPx(purposeId) { if (purposeId === "text") return { lo: 18, hi: 21, def: 20 }; if (purposeId === "presentation") return { lo: 28, hi: 32, def: 32 }; return { lo: 22, hi: 25, def: 24 }; // balanced — the default } // Mirrors references/canvas-formats.md § "Typography Scale Start". The // 3× short-side cap keeps extreme aspect ratios from inflating the scale; // lo/hi remain advisory and def is the initial anchor, never a hard floor. function bodySizeBandForCanvas(canvasVal, purposeId) { if (isPptCanvas(canvasVal)) return deliveryBodyPx(purposeId); var dims = canvasDimensions(canvasVal); if (!dims) return { lo: 32, hi: 48, def: 40 }; // legacy invalid-custom fallback var shortSide = Math.min(dims.width, dims.height); var longSide = Math.max(dims.width, dims.height); var span = Math.min(longSide, 3 * shortSide); return { lo: Math.round(span * 0.025), hi: Math.round(span * 0.033), def: Math.round(span * 0.029) }; } function defaultBodySizeForCanvas(canvasVal, purposeId) { return bodySizeBandForCanvas(canvasVal, purposeId).def; } // Resolve the only deterministic same-stage size dependency locally. The // backend authors Stage 2 once; changing reading mode or body size updates // only unpinned values already visible in this page. function syncUnpinnedTypographySizes(resetBodyFromReadingMode) { if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; if (!STATE.typography.sizes) STATE.typography.sizes = {}; if (resetBodyFromReadingMode && !TYPOGRAPHY_SIZE_OVERRIDES.body) { STATE.typography.body_size = defaultBodySizeForCanvas( STATE.canvas, STATE.delivery_purpose ); } var body = parseFloat(STATE.typography.body_size); if (!isFinite(body)) { body = defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose); } SIZE_ROLES.forEach(function (role) { if (!TYPOGRAPHY_SIZE_OVERRIDES[role]) { STATE.typography.sizes[role] = deriveSize(role, body); } }); refreshSizeInputs(); refreshBodySizeHint(); refreshStylePreview(); } function roundSize(value) { return Math.round(value * 100) / 100; } function formatPtFromPx(value) { var px = parseFloat(value); if (!isFinite(px)) return ""; var pt = Math.round(px * 0.75 * 10) / 10; return pt % 1 === 0 ? String(Math.round(pt)) : String(pt); } function normalizeTypographyForSubmit(payload) { if (!payload.typography || typeof payload.typography !== "object") return; var typ = normTypography(payload.typography); payload.typography = typ; if (isEnglishProject()) { ["heading", "body"].forEach(function (role) { delete typ[role].english; delete typ[role].sample_english; }); } var body = parseFloat(typ.body_size); if (!isFinite(body)) { // Cleared / invalid body field — fall back so role sizes never submit // against an empty anchor. body = defaultBodySizeForCanvas(payload.canvas, payload.delivery_purpose); } // px is the only unit — round and submit as-is. No pt conversion, no // body_size_pt / sizes_pt provenance (the system never carries pt). typ.body_size = roundSize(body); typ.body_size_unit = "px"; if (typ.sizes && typeof typ.sizes === "object") { Object.keys(typ.sizes).forEach(function (role) { var raw = parseFloat(typ.sizes[role]); if (isFinite(raw)) typ.sizes[role] = roundSize(raw); }); } if (typographyFamiliesComplete(typ)) delete typ.custom; // delivery_purpose is PPT-only; drop it on non-PPT canvases where it has // no meaning and was never shown. if (!isPptCanvas(payload.canvas)) delete payload.delivery_purpose; } function renderColor(host) { var cands = colorRecommendationCandidates(); var sec = section(5, "sec_color"); var grid = el("div", "color-grid"); var hexInputs = {}; var hexSwatches = {}; var cardSwatchRefs = []; // idx -> {role: swatchEl}, for live override feedback var selectedIdx = -1; function paintSwatch(elem, val) { var n = normHex(val); elem.style.background = n || "transparent"; elem.classList.toggle("hex-swatch-empty", !n); } function applyHexInputs(pal) { PALETTE_ROLES.forEach(function (role) { if (hexInputs[role]) hexInputs[role].value = pal[role] || ""; if (hexSwatches[role]) paintSwatch(hexSwatches[role], pal[role]); }); } var customInput = el("textarea", "text-input custom-color-input"); setNaturalInputDirection(customInput); customInput.rows = 2; customInput.placeholder = t("custom_color_placeholder"); customInput.style.display = "none"; function selectCard(idx) { var c = cands[idx] || {}; selectedIdx = idx; STATE.color = { name: localized(c, "name") || c.name || "", palette: Object.assign({}, normPalette(c)) }; grid.querySelectorAll(".color-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); }); customInput.style.display = "none"; applyHexInputs(STATE.color.palette); refreshStylePreview(); } function selectCustomColor() { selectedIdx = -1; STATE.color = { name: "custom", custom: customInput.value || "", palette: {} }; grid.querySelectorAll(".color-card").forEach(function (card) { card.classList.remove("selected"); }); customCard.classList.add("selected"); customInput.style.display = "block"; customInput.focus(); refreshStylePreview(); } cands.forEach(function (c, idx) { var pal = normPalette(c); var refs = {}; var card = el("div", "color-card"); var sw = el("div", "swatches"); PALETTE_ROLES.forEach(function (role) { if (!pal[role]) return; var col = el("div", "swatch-col"); var s = el("div", "swatch"); s.style.background = pal[role]; refs[role] = s; col.appendChild(s); col.appendChild(el("div", "swatch-role", t("role_" + role))); col.appendChild(el("div", "color-hex", normHex(pal[role]) || pal[role])); sw.appendChild(col); }); cardSwatchRefs[idx] = refs; card.appendChild(sw); card.appendChild(el("div", "color-name", localized(c, "name") || (t("option_prefix") + " " + (idx + 1)))); if (localized(c, "note")) card.appendChild(el("div", "color-note", localized(c, "note"))); card.addEventListener("click", function () { selectCard(idx); }); grid.appendChild(card); }); var customCard = el("div", "color-card color-card-custom"); customCard.appendChild(el("div", "color-name", t("custom_color"))); customCard.addEventListener("click", selectCustomColor); grid.appendChild(customCard); sec.appendChild(grid); customInput.addEventListener("input", function () { if (!STATE.color || STATE.color.name !== "custom") selectCustomColor(); STATE.color.custom = customInput.value; refreshStylePreview(); }); sec.appendChild(customInput); var override = el("div", "hex-override"); override.appendChild(el("div", "subfield-label", t("hex_override"))); var row = el("div", "hex-row"); PALETTE_ROLES.forEach(function (role) { var wrap = el("div", "hex-cell"); wrap.appendChild(el("div", "hex-cell-label", t("role_" + role))); var line = el("div", "hex-input-line"); var sw = el("div", "hex-swatch hex-swatch-empty"); var inp = document.createElement("input"); inp.type = "text"; inp.placeholder = "#"; inp.addEventListener("input", function () { if (!STATE.color) STATE.color = { name: "custom", palette: {} }; if (!STATE.color.palette) STATE.color.palette = {}; STATE.color.palette[role] = inp.value; paintSwatch(sw, inp.value); // Reflect a valid override straight onto the selected card so the // user sees the change in context, not just in the input row. var n = normHex(inp.value); if (n && selectedIdx >= 0 && cardSwatchRefs[selectedIdx] && cardSwatchRefs[selectedIdx][role]) { cardSwatchRefs[selectedIdx][role].style.background = n; } refreshStylePreview(); }); hexInputs[role] = inp; hexSwatches[role] = sw; line.appendChild(sw); line.appendChild(inp); wrap.appendChild(line); row.appendChild(wrap); }); override.appendChild(row); sec.appendChild(override); host.appendChild(sec); var selIdx = -1; if (STATE.color && STATE.color.name && STATE.color.name !== "custom") { cands.forEach(function (c, i) { if ((localized(c, "name") || c.name) === STATE.color.name) selIdx = i; }); } if (STATE.color && STATE.color.name === "custom") { customInput.value = STATE.color.custom || ""; selectCustomColor(); } else if (selIdx >= 0) { selectCard(selIdx); } else { applyHexInputs((STATE.color && STATE.color.palette) || {}); } } function renderIcons(host) { var sec = section(6, "sec_icons"); enumField(sec, CAT.icons, recOrFirst("icons", CAT.icons), function () { return STATE.icons; }, function (v) { STATE.icons = v; refreshStylePreview(); }, { allowCustom: true, customOnOwnRow: true, ownRowIds: ["none"] }); host.appendChild(sec); } function previewFontStack(primary, fallback) { if (!primary) return fallback || ""; if (!fallback) return primary; return primary + ", " + fallback; } function typographyFamiliesComplete(typography) { typography = normTypography(typography || {}); return ["heading", "body"].every(function (role) { var font = typography[role] || {}; var fields = isEnglishProject() ? ["primary", "css"] : ["primary", "english", "css"]; return fields.every(function (field) { return !!String(font[field] || "").trim(); }); }); } function sampleText(role, field) { // Keep comparison copy stable: choices change visual treatment, not content. var useEnglish = field === "english" || isEnglishProject(); if (role === "heading") { return t(useEnglish ? "preview_latin_title" : "preview_big_title"); } return t(useEnglish ? "preview_latin_body" : "preview_body_intro"); } function fontSample(box, slot, css, role) { var line = el("div", "font-sample-line"); var primary = el("span", "fs-primary", sampleText(role, "primary")); setUiLanguageAttributes(primary); var primaryStack = previewFontStack(slot.primary, css); if (primaryStack) primary.style.fontFamily = primaryStack; if (primaryStack) primary.title = primaryStack; line.appendChild(primary); if (!isEnglishProject()) { var english = el("span", "fs-english", sampleText(role, "english")); setEnglishLanguageAttributes(english); var englishStack = previewFontStack(slot.english, css); if (englishStack) english.style.fontFamily = englishStack; if (englishStack) english.title = englishStack; line.appendChild(english); } box.appendChild(line); } function fontChoiceLabel(font) { var label = localized(font, "label") || font.id; if (String(label).toLowerCase() === String(font.id).toLowerCase()) { return String(font.id); } return label + " · " + font.id; } function typographyFieldLabel(field) { if (field === "english") return t("english_font"); return t("primary_language_font"); } function typographyChoiceConfigs() { var configs = []; ["heading", "body"].forEach(function (role) { configs.push([role, "primary"]); if (!isEnglishProject()) configs.push([role, "english"]); }); return configs; } function typographySignature(typography) { typography = normTypography(typography || {}); var fields = isEnglishProject() ? ["primary"] : ["primary", "english"]; var values = []; ["heading", "body"].forEach(function (role) { fields.forEach(function (field) { values.push(String((typography[role] || {})[field] || "") .trim().toLowerCase()); }); }); return values.join("\u0000"); } function renderTypography(host) { var cands = typographyRecommendationCandidates().filter(typographyFamiliesComplete); var sec = section(7, "sec_type"); var grid = el("div", "font-grid"); var customFields = el("div", "custom-typography-fields font-picker-fields"); var customInputs = {}; var customLegacyNote = el("div", "toggle-desc custom-typography-legacy"); var pickerHead = el("div", "font-picker-head"); pickerHead.appendChild(el("span", "subfield-label", t("font_selection"))); var customStatus = el("span", "font-custom-status", t("customized")); pickerHead.appendChild(customStatus); customFields.appendChild(pickerHead); customFields.appendChild(el("div", "toggle-desc font-picker-hint", t("font_picker_hint"))); customFields.appendChild(customLegacyNote); function syncCustomInputs() { var typography = STATE.typography || {}; customLegacyNote.textContent = String(typography.custom || "").trim(); customLegacyNote.style.display = customLegacyNote.textContent ? "block" : "none"; customStatus.style.display = typography.name === "custom" ? "inline-flex" : "none"; typographyChoiceConfigs().forEach(function (config) { var role = config[0], field = config[1]; var control = customInputs[role + "_" + field]; if (!control) return; var value = String((typography[role] || {})[field] || ""); var matchingFont = control.options.filter(function (font) { return String(font.id).toLowerCase() === value.toLowerCase(); })[0]; control.select.value = matchingFont ? matchingFont.id : "__other__"; control.other.value = matchingFont ? "" : value; control.other.style.display = matchingFont ? "none" : "block"; }); } function markCustomTypography() { if (!STATE.typography) { STATE.typography = { name: "custom", heading: {}, body: {} }; } STATE.typography.name = "custom"; grid.querySelectorAll(".font-card").forEach(function (card) { card.classList.remove("selected"); }); customCard.classList.add("selected"); customStatus.style.display = "inline-flex"; } function updateRoleCss(role) { if (!STATE.typography[role]) STATE.typography[role] = {}; STATE.typography[role].css = derivedFontCss(STATE.typography[role]); } function selectFont(idx) { var c = normTypography(cands[idx] || {}); var prev = STATE.typography || {}; STATE.typography = { name: localized(c, "name") || c.name || "", heading: c.heading || {}, body: c.body || {}, // Font cards choose family and character. Reading mode and // explicit size inputs own the sizing state. body_size: prev.body_size || defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose), sizes: Object.assign({}, prev.sizes || {}) }; if (sizeInput) sizeInput.value = STATE.typography.body_size || ""; grid.querySelectorAll(".font-card").forEach(function (card, i) { card.classList.toggle("selected", i === idx); }); syncCustomInputs(); refreshSizeInputs(); // fill any role with no value yet; never overwrites existing values refreshStylePreview(); } function selectCustomTypography() { var prev = STATE.typography || {}; STATE.typography = normTypography(prev); STATE.typography.name = "custom"; STATE.typography.body_size = prev.body_size || ""; // Switching font family must not drop any explicit size. STATE.typography.sizes = Object.assign({}, prev.sizes || {}); markCustomTypography(); syncCustomInputs(); var firstControl = customInputs.heading_primary; if (firstControl) firstControl.select.focus(); refreshSizeInputs(); refreshStylePreview(); } cands.forEach(function (c, idx) { c = normTypography(c); var head = c.heading || {}, body = c.body || {}; var card = el("div", "font-card"); var top = el("div", "font-card-head"); top.appendChild(el("span", "font-card-name", localized(c, "name") || (t("option_prefix") + " " + (idx + 1)))); var metaFields = isEnglishProject() ? ["primary"] : ["primary", "english"]; var meta = ["heading", "body"].map(function (role) { var slot = role === "heading" ? head : body; return t("font_" + role) + " " + metaFields.map(function (field) { return typographyFieldLabel(field) + ": " + (slot[field] || "—"); }).join(" / "); }).join(" · "); top.appendChild(el("span", "font-card-meta", meta)); card.appendChild(top); var hbox = el("div", "font-sample-heading-box"); fontSample(hbox, head, head.css, "heading"); card.appendChild(hbox); var bbox = el("div", "font-sample-body-box"); fontSample(bbox, body, body.css, "body"); card.appendChild(bbox); if (localized(c, "note")) card.appendChild(el("div", "color-note", localized(c, "note"))); card.addEventListener("click", function () { selectFont(idx); }); grid.appendChild(card); }); var customCard = el("div", "font-card font-card-custom"); customCard.appendChild(el("div", "font-card-name", t("custom_typography"))); customCard.addEventListener("click", selectCustomTypography); grid.appendChild(customCard); sec.appendChild(grid); typographyChoiceConfigs().forEach(function (config) { var role = config[0], fieldName = config[1]; var field = el("label", "custom-typography-field"); field.appendChild(el("span", "hex-cell-label", t("font_" + role) + " · " + typographyFieldLabel(fieldName))); var options = fontCatalogEntries(fieldName); var select = el("select", "font-select"); options.forEach(function (font) { var option = document.createElement("option"); option.value = font.id; option.textContent = fontChoiceLabel(font); select.appendChild(option); }); var otherOption = document.createElement("option"); otherOption.value = "__other__"; otherOption.textContent = t("other_installed_font"); select.appendChild(otherOption); var input = el("input", "text-input other-font-input"); input.type = "text"; input.placeholder = t("other_font_placeholder"); select.addEventListener("change", function () { markCustomTypography(); if (!STATE.typography[role]) { STATE.typography[role] = { css: "sans-serif" }; } if (select.value === "__other__") { input.value = ""; input.style.display = "block"; STATE.typography[role][fieldName] = ""; input.focus(); } else { input.value = ""; input.style.display = "none"; STATE.typography[role][fieldName] = select.value; } updateRoleCss(role); refreshStylePreview(); }); input.addEventListener("input", function () { markCustomTypography(); if (!STATE.typography[role]) { STATE.typography[role] = { css: "sans-serif" }; } STATE.typography[role][fieldName] = input.value; updateRoleCss(role); refreshStylePreview(); }); customInputs[role + "_" + fieldName] = { select: select, other: input, options: options }; field.appendChild(select); field.appendChild(input); customFields.appendChild(field); }); sec.appendChild(customFields); var sizeField = el("div", "subfield"); sizeField.appendChild(el("div", "subfield-label", t("font_body_size"))); sizeField.appendChild(el("div", "toggle-desc body-size-relation", t("body_size_unit_relation"))); var sizeRow = el("div", "font-size-row"); var sizeInput = el("input", "num-input font-size-input"); sizeInput.type = "number"; sizeInput.min = "8"; sizeInput.max = "256"; sizeInput.step = "1"; sizeInput.value = (STATE.typography && STATE.typography.body_size) || ""; sizeInput.placeholder = isPptCanvas(STATE.canvas) ? "20 / 24 / 32" : String(bodySizeBandForCanvas(STATE.canvas, STATE.delivery_purpose).def); sizeInput.addEventListener("input", function () { if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; STATE.typography.body_size = sizeInput.value; TYPOGRAPHY_SIZE_OVERRIDES.body = sizeInput.value !== ""; // Body is an explicit local anchor. Recompute only role values the // user has not edited; no request leaves the browser. syncUnpinnedTypographySizes(false); }); sizeRow.appendChild(sizeInput); sizeRow.appendChild(el("span", "font-size-unit", "px")); var sizePtHint = el("div", "toggle-desc body-size-pt"); var sizeHint = el("div", "toggle-desc body-size-hint"); // PPT body is one fixed px value per reading mode (not a range); non-PPT // canvases use the canvas-owned effective span. A manually pinned value is never // overwritten by later reading-mode changes. // Everything is px — lo/hi are only a sanity envelope for the OOR flag. refreshBodySizeHint = function () { var txt = t("font_body_size_hint"); var lo, hi; if (isPptCanvas(STATE.canvas)) { var pb = deliveryBodyPx(STATE.delivery_purpose); lo = pb.lo; hi = pb.hi; txt += " " + t("body_size_hint_purpose").replace("{def}", pb.def); } else { var band = bodySizeBandForCanvas(STATE.canvas, STATE.delivery_purpose); lo = band.lo; hi = band.hi; txt += " " + t("body_size_hint_canvas") .replace("{lo}", lo).replace("{hi}", hi); } // Flag (hint only) a value far outside the // canvas's usual px range, so an accidental extreme value is visible // instead of silently submitting it. var cur = parseFloat(STATE.typography && STATE.typography.body_size); sizePtHint.textContent = isFinite(cur) ? t("body_size_pt_hint").replace("{pt}", formatPtFromPx(cur)) : ""; if (isFinite(cur) && isFinite(lo) && isFinite(hi) && (cur < lo || cur > hi)) { txt += " " + t("body_size_hint_oor"); } sizeHint.textContent = txt; }; refreshBodySizeHint(); sizeField.appendChild(sizeRow); sizeField.appendChild(sizePtHint); sizeField.appendChild(sizeHint); // Reading mode and typography are both confirmed in Stage 2. Its // compatibility key remains delivery_purpose; the dependency is a local // deterministic update, not a second Stage-2 recommendation. sec.appendChild(sizeField); // Per-role size override (parallel to color's per-role HEX override): the // ramp derives title / subtitle / annotation from body, but the user may // set each explicitly. Values are px (the system's only unit). var sizeOverride = el("div", "hex-override"); sizeOverride.appendChild(el("div", "subfield-label", t("size_override"))); var srow = el("div", "hex-row"); var sizeInputs = {}; var sizePtHints = {}; function refreshRolePtHint(role) { var input = sizeInputs[role]; var hint = sizePtHints[role]; if (!input || !hint) return; var pt = formatPtFromPx(input.value); hint.textContent = pt ? t("role_size_pt_hint").replace("{pt}", pt) : ""; } SIZE_ROLES.forEach(function (role) { var wrap = el("div", "hex-cell"); wrap.appendChild(el("div", "hex-cell-label", t("size_role_" + role))); var inputLine = el("div", "role-size-line"); var inp = document.createElement("input"); inp.type = "number"; inp.min = "6"; inp.max = "512"; inp.step = "1"; inp.addEventListener("input", function () { if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; if (!STATE.typography.sizes) STATE.typography.sizes = {}; STATE.typography.sizes[role] = inp.value; TYPOGRAPHY_SIZE_OVERRIDES[role] = true; refreshRolePtHint(role); refreshStylePreview(); }); sizeInputs[role] = inp; inputLine.appendChild(inp); inputLine.appendChild(el("span", "font-size-unit", "px")); wrap.appendChild(inputLine); sizePtHints[role] = el("div", "role-size-pt"); wrap.appendChild(sizePtHints[role]); srow.appendChild(wrap); }); sizeOverride.appendChild(srow); sec.appendChild(sizeOverride); // Reflect state into the controls. Derivation itself happens only through // syncUnpinnedTypographySizes(); a re-render preserves the visible state. refreshSizeInputs = function () { if (!STATE.typography) STATE.typography = { name: "", heading: {}, body: {} }; if (!STATE.typography.sizes) STATE.typography.sizes = {}; sizeInput.value = STATE.typography.body_size || ""; var bodyVal = parseFloat(STATE.typography.body_size) || defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose); SIZE_ROLES.forEach(function (role) { var cur = STATE.typography.sizes[role]; var hasVal = cur !== undefined && cur !== null && cur !== ""; if (!hasVal) STATE.typography.sizes[role] = deriveSize(role, bodyVal); if (sizeInputs[role]) sizeInputs[role].value = STATE.typography.sizes[role]; refreshRolePtHint(role); }); }; refreshSizeInputs(); host.appendChild(sec); var nameMatch = -1; var signatureMatch = -1; var stateSignature = typographySignature(STATE.typography || {}); if (STATE.typography && STATE.typography.name !== "custom") cands.forEach(function (c, i) { var sameName = [localized(c, "name"), c.name_zh, c.name_zh_tw, c.name_en, c.name_ja] .some(function (name) { return name === STATE.typography.name; }); if (!sameName && c.name && typeof c.name === "object") { sameName = Object.keys(c.name).some(function (key) { return c.name[key] === STATE.typography.name; }); } if (sameName && nameMatch < 0) nameMatch = i; if (typographySignature(c) === stateSignature && signatureMatch < 0) signatureMatch = i; }); // Names preserve the selected candidate when several recommendations share // one font stack. Signature matching is only a first-match legacy fallback. var selIdx = nameMatch >= 0 ? nameMatch : signatureMatch; if (selIdx >= 0) selectFont(selIdx); else if (STATE.typography && STATE.typography.name === "custom") { ["heading", "body"].forEach(function (role) { if (!STATE.typography[role]) STATE.typography[role] = {}; if (!STATE.typography[role].css) updateRoleCss(role); }); customCard.classList.add("selected"); syncCustomInputs(); } else { syncCustomInputs(); } } function renderFormulaPolicy(host) { var sec = section("F", "formula_policy"); enumField(sec, CAT.formula_policy, recOrFirst("formula_policy", CAT.formula_policy), function () { return STATE.formula_policy; }, function (v) { STATE.formula_policy = v; }); host.appendChild(sec); } // Combined color + typography + icon preview — not a separate confirmation, just a // live "overall impression" of the style choices made above. Kept // deliberately abstract (a style chip, not a slide layout); page layout // preview is the live-preview server's job (Step 6). function renderStylePreview(host) { var wrap = el("div", "style-preview"); var label = el("div", "style-preview-label"); label.appendChild(el("span", "spl-title", t("style_preview_label"))); // The "rough feel, not a slide layout" caveat sits in the label in the // UI font — never rendered in the candidate's body font, so it cannot // pose as sample content. label.appendChild(el("span", "spl-note", t("style_preview_body"))); wrap.appendChild(label); var card = el("div", "style-preview-card"); var textcol = el("div", "sp-textcol"); var title = el("div", "sp-title"); var titlePrimary = el("span", "sp-title-primary"); var titleEnglish = el("span", "sp-title-english"); title.appendChild(titlePrimary); title.appendChild(titleEnglish); var bodyRow = el("div", "sp-body"); var accentBar = el("span", "sp-accent-bar"); var bodyWrap = el("div", "sp-body-wrap"); var bodyPrimary = el("span", "sp-body-primary"); var bodyEnglish = el("span", "sp-body-english"); setEnglishLanguageAttributes(titleEnglish); setEnglishLanguageAttributes(bodyEnglish); bodyWrap.appendChild(bodyPrimary); bodyWrap.appendChild(bodyEnglish); bodyRow.appendChild(accentBar); bodyRow.appendChild(bodyWrap); textcol.appendChild(title); textcol.appendChild(bodyRow); var content = el("div", "sp-content"); var chip = el("div", "sp-chip"); var chipDot = el("span", "sp-chip-dot"); var chipLabel = el("span", "sp-chip-label"); chip.appendChild(chipDot); chip.appendChild(chipLabel); card.appendChild(textcol); card.appendChild(content); card.appendChild(chip); wrap.appendChild(card); host.appendChild(wrap); // The strip is mounted inside the top bar on Stage 2, so it stays visible // while the center form scrolls. wrap.style.top = "0px"; function paint() { var pal = (STATE.color && STATE.color.palette) || {}; var typ = STATE.typography || {}; var head = typ.heading || {}, body = typ.body || {}; var bg = hexOr(pal.background, "#ffffff"); var sbg = hexOr(pal.secondary_bg, bg); var pri = hexOr(pal.primary, "#1a3a6b"); var acc = hexOr(pal.accent, pri); var sacc = hexOr(pal.secondary_accent, acc); var txt = hexOr(pal.body_text, "#1d2430"); // body_size is px everywhere — preview it directly, no conversion. var rawSize = parseFloat(typ.body_size) || defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose); var bodyPx = Math.max(12, Math.min(34, rawSize)); var headPrimaryStack = previewFontStack(head.primary, head.css); var headEnglishStack = previewFontStack(head.english, head.css); var bodyPrimaryStack = previewFontStack(body.primary, body.css); var bodyEnglishStack = previewFontStack(body.english, body.css); card.style.background = bg; titlePrimary.textContent = sampleText("heading", "primary"); setUiLanguageAttributes(titlePrimary); titleEnglish.textContent = sampleText("heading", "english"); title.style.color = pri; title.style.fontSize = Math.round(bodyPx * 1.7) + "px"; titlePrimary.style.fontFamily = headPrimaryStack || ""; titleEnglish.style.fontFamily = headEnglishStack || ""; bodyPrimary.textContent = sampleText("body", "primary"); setUiLanguageAttributes(bodyPrimary); bodyEnglish.textContent = sampleText("body", "english"); bodyWrap.style.color = txt; bodyWrap.style.fontSize = bodyPx + "px"; bodyPrimary.style.fontFamily = bodyPrimaryStack || ""; bodyEnglish.style.fontFamily = bodyEnglishStack || ""; titleEnglish.style.display = isEnglishProject() ? "none" : ""; bodyEnglish.style.display = isEnglishProject() ? "none" : ""; accentBar.style.background = acc; content.style.color = txt; content.style.fontFamily = bodyPrimaryStack || ""; content.innerHTML = stylePreviewContentMarkup(STATE.icons); setUiLanguageAttributes(content); chip.style.background = sbg; chipDot.style.background = sacc; chipLabel.textContent = t("role_secondary_bg"); chipLabel.style.color = txt; chipLabel.style.fontFamily = bodyPrimaryStack || ""; } refreshStylePreview = paint; paint(); } function renderImageStrategyPreview(host) { var wrap = el("div", "style-preview image-strategy-left-preview"); var label = el("div", "style-preview-label"); label.appendChild(el("span", "spl-title", t("image_strategy"))); label.appendChild(el("span", "spl-note", t("image_strategy_reference_hint"))); wrap.appendChild(label); var card = el("div", "style-preview-card image-strategy-preview-card"); var visual = el("div", "image-strategy-preview-visual"); var copy = el("div", "direction-preview-copy"); var title = el("div", "direction-preview-title"); var desc = el("div", "direction-preview-desc"); copy.appendChild(title); copy.appendChild(desc); card.appendChild(visual); card.appendChild(copy); wrap.appendChild(card); host.appendChild(wrap); function paint() { var show = needsGeneratedImagesForUsage(STATE.image_usage); wrap.hidden = !show; if (!show) return; var strategy = STATE.image_strategy || {}; visual.innerHTML = ""; var row = appendImageStrategyPreviews(visual, strategy); visual.classList.toggle("image-strategy-preview-empty", !row); if (!row) visual.appendChild(el("div", "toggle-desc", strategy.rendering === "custom" ? t("image_strategy_no_reference") : t("image_strategy_select_placeholder"))); title.textContent = strategy.name || (strategy.rendering === "custom" ? t("image_strategy_ai_custom") : (strategy.rendering ? comparisonValueLabel("rendering", strategy.rendering) : t("image_strategy_select_placeholder"))); var parts = []; if (strategy.rendering) { parts.push(t("image_strategy_rendering") + ": " + comparisonValueLabel("rendering", strategy.rendering)); } if (strategy.visual) parts.push(t("image_strategy_visual") + ": " + strategy.visual); if (strategy.mood) parts.push(t("image_strategy_mood") + ": " + strategy.mood); if (strategy.behavior) parts.push(strategy.behavior); desc.textContent = parts.join(" · ") || t("image_strategy_reference_hint"); } refreshImageStrategyPreview = paint; paint(); } function findCatalogOption(list, id) { var flat = []; (list || []).forEach(function (item) { if (item && item.items) flat = flat.concat(item.items); else flat.push(item); }); for (var i = 0; i < flat.length; i += 1) { if (flat[i] && flat[i].id === id) return flat[i]; } return null; } function stylePreviewRows() { return [ [t("preview_point_1_title"), t("preview_point_1_text")], [t("preview_point_2_title"), t("preview_point_2_text")], [t("preview_point_3_title"), t("preview_point_3_text")] ]; } function stylePreviewContentMarkup(iconId) { var rows = stylePreviewRows(); var icons = stylePreviewIconSamples(iconId, rows.length); return rows.map(function (row, idx) { return '
' + '' + icons[idx] + '' + '' + escapeHtml(row[0]) + '' + escapeHtml(row[1]) + '' + '
'; }).join(""); } function stylePreviewIconSamples(iconId, count) { if (iconId === "emoji") return ["📊", "💡", "✅"].slice(0, count).map(function (x) { return '' + x + ''; }); if (iconId === "none") return new Array(count).fill(''); var samples = ICON_PREVIEWS[iconId] || []; var out = []; for (var i = 0; i < count; i += 1) { var sample = samples[i % Math.max(samples.length, 1)]; out.push(sample ? '' + (sample.svg || "") + '' : ''); } return out; } function renderImageDirection(host) { var sec = section(8, "sec_images"); var usageChips = el("div", "chips"); var usageNote = el("div", "subfield"); usageNote.appendChild(el("div", "subfield-label", t("image_usage_notes"))); var usageNoteInput = el("textarea", "text-input image-usage-notes-input"); setNaturalInputDirection(usageNoteInput); usageNoteInput.placeholder = t("image_usage_notes_placeholder"); usageNoteInput.value = STATE.image_notes || ""; usageNoteInput.addEventListener("input", function () { STATE.image_notes = usageNoteInput.value; }); usageNote.appendChild(usageNoteInput); var strategySub = el("div", "subfield image-strategy-subfield"); strategySub.appendChild(el("div", "subfield-label", t("image_strategy"))); strategySub.appendChild(el("div", "toggle-desc", t("image_strategy_reference_hint"))); renderDirectionComponentCandidates(strategySub, "image_strategy"); var recommendedStrategies = imageStrategyRecommendationCandidates(); var strategyCands = imageStrategySelectableCandidates(); var hasRecommendedStrategies = recommendedStrategies.length > 0; var customStrategy = STATE.image_strategy_custom || imageStrategyCustomCandidate(); var presetPicker = el("div", "image-strategy-picker"); var presetSelect = el("select", "font-select image-strategy-select"); var customCard = null; var currentCustomEditor = null; var syncCustomStrategy = function () {}; var selectCustomImageStrategy = function () {}; function selectImageStrategy(idx) { if (!strategyCands[idx]) return; STATE.image_strategy = normalizedImageStrategy(strategyCands[idx]); ACTIVE_COMPONENT_DIRECTION_IDS.image_strategy = ""; presetSelect.value = String(idx); if (customCard) customCard.classList.remove("selected"); if (currentCustomEditor) currentCustomEditor.style.display = "none"; syncCustomStrategy(false); refreshImageStrategyPreview(); refreshDesignDirectionState(); refreshDirectionComponentStates(); } function imageStrategyCandidateIndex(strategy) { if (!strategy) return -1; var normalized = JSON.stringify(comparableImageStrategy(strategy)); for (var i = 0; i < strategyCands.length; i += 1) { if (JSON.stringify(comparableImageStrategy(strategyCands[i])) === normalized) return i; } for (var fallback = 0; fallback < strategyCands.length; fallback += 1) { if (strategyCands[fallback] && strategyCands[fallback].rendering === strategy.rendering) return fallback; } return -1; } function strategyOptionLabel(candidate, idx) { var renderingLabel = comparisonValueLabel("rendering", candidate.rendering); var candidateName = localized(candidate, "name") || renderingLabel || (t("option_prefix") + " " + (idx + 1)); return candidateName !== renderingLabel ? candidateName + " · " + renderingLabel : candidateName; } function appendStrategyOptions(label, start, end) { if (start >= end) return; var group = document.createElement("optgroup"); group.label = label; for (var idx = start; idx < end; idx += 1) { var option = document.createElement("option"); option.value = String(idx); option.textContent = strategyOptionLabel(strategyCands[idx], idx); group.appendChild(option); } presetSelect.appendChild(group); } var placeholderOption = document.createElement("option"); placeholderOption.value = ""; placeholderOption.textContent = t("image_strategy_select_placeholder"); placeholderOption.disabled = true; placeholderOption.selected = true; presetSelect.appendChild(placeholderOption); appendStrategyOptions(t("image_strategy_recommended_group"), 0, recommendedStrategies.length); appendStrategyOptions(t("image_strategy_all_group"), recommendedStrategies.length, strategyCands.length); presetSelect.disabled = !strategyCands.length; presetSelect.addEventListener("change", function () { selectImageStrategy(parseInt(presetSelect.value, 10)); }); presetPicker.appendChild(presetSelect); if (!strategyCands.length) { presetPicker.appendChild(el("div", "toggle-desc", t("image_strategy_empty"))); } strategySub.appendChild(presetPicker); if (customStrategy) { customStrategy = normalizedImageStrategy(customStrategy); STATE.image_strategy_custom = customStrategy; customCard = el("div", "font-card image-strategy-custom-card ai-custom-candidate"); var customTop = el("div", "font-card-head"); customTop.appendChild(el("span", "font-card-name", customStrategy.name || t("image_strategy_ai_custom"))); customTop.appendChild(el("span", "font-card-meta", t("image_strategy_ai_custom_desc"))); customCard.appendChild(customTop); [ ["image_strategy_visual", customStrategy.visual], ["image_strategy_mood", customStrategy.mood] ].forEach(function (row) { if (row[1]) customCard.appendChild(el("div", "color-note", t(row[0]) + ":" + row[1])); }); var customCopy = el("div", "ai-custom-candidate-copy", customStrategy.behavior || t("image_strategy_custom_placeholder")); customCard.appendChild(customCopy); var customInput = el("textarea", "text-input image-strategy-custom-input"); setNaturalInputDirection(customInput); customInput.rows = 4; customInput.placeholder = t("image_strategy_custom_placeholder"); customInput.value = customStrategy.behavior; customInput.style.display = "none"; customCard.appendChild(customInput); syncCustomStrategy = function (selected) { customCopy.textContent = customStrategy.behavior || t("image_strategy_custom_placeholder"); customCopy.style.display = selected ? "none" : "block"; customInput.style.display = selected ? "block" : "none"; if (selected && customInput.value !== customStrategy.behavior) { customInput.value = customStrategy.behavior || ""; } }; selectCustomImageStrategy = function () { STATE.image_strategy = normalizedImageStrategy(customStrategy); ACTIVE_COMPONENT_DIRECTION_IDS.image_strategy = ""; presetSelect.value = ""; customCard.classList.add("selected"); syncCustomStrategy(true); refreshImageStrategyPreview(); refreshDesignDirectionState(); refreshDirectionComponentStates(); }; customInput.addEventListener("click", function (event) { event.stopPropagation(); }); customInput.addEventListener("input", function () { customStrategy.behavior = customInput.value; STATE.image_strategy_custom = normalizedImageStrategy(customStrategy); selectCustomImageStrategy(); }); customCard.addEventListener("click", function () { selectCustomImageStrategy(); customInput.focus(); }); syncCustomStrategy(false); strategySub.appendChild(customCard); } if (!customCard && !ACTIVE_COMPONENT_DIRECTION_IDS.image_strategy && STATE.image_strategy && STATE.image_strategy.rendering === "custom") { var customEditorField = el("div", "subfield image-strategy-current-custom"); customEditorField.appendChild(el("div", "subfield-label", t("custom"))); currentCustomEditor = el("textarea", "text-input image-strategy-custom-input"); setNaturalInputDirection(currentCustomEditor); currentCustomEditor.rows = 4; currentCustomEditor.placeholder = t("image_strategy_custom_placeholder"); currentCustomEditor.value = STATE.image_strategy.behavior || ""; currentCustomEditor.addEventListener("input", function () { STATE.image_strategy.behavior = currentCustomEditor.value; refreshImageStrategyPreview(); refreshDesignDirectionState(); refreshDirectionComponentStates(); }); customEditorField.appendChild(currentCustomEditor); strategySub.appendChild(customEditorField); } var recommendedIds = selectedImageUsageIds(recValue("image_usage")); if (!recommendedIds.length) recommendedIds = [defaultImageUsageId()]; var usageChipById = {}; function refreshUsageChips() { Object.keys(usageChipById).forEach(function (id) { usageChipById[id].classList.toggle("selected", STATE.image_usage.indexOf(id) >= 0); }); var noImages = STATE.image_usage.indexOf("none") >= 0; usageNote.style.display = noImages ? "none" : "block"; strategySub.style.display = needsGeneratedImagesForUsage(STATE.image_usage) ? "block" : "none"; refreshImageStrategyPreview(); } function toggleImageUsage(id) { var current = STATE.image_usage.slice(); if (id === "none") { current = current.indexOf("none") >= 0 ? [] : ["none"]; } else { current = current.filter(function (item) { return item !== "none"; }); if (current.indexOf(id) >= 0) { current = current.filter(function (item) { return item !== id; }); } else { current.push(id); } } STATE.image_usage = current; refreshUsageChips(); refreshImageProduction(); } (CAT.image_usage || []).forEach(function (option) { var label = optionLabel(option); var desc = optionDesc(option); if (desc) label += (LANG === "zh" || LANG === "ja" ? ":" : " — ") + desc; var chip = el("div", "chip"); chip.appendChild(el("span", "chip-text", label)); if (recommendedIds.indexOf(option.id) >= 0) { chip.classList.add("recommended"); chip.appendChild(el("span", "rec-badge", "★ " + t("recommended"))); } chip.addEventListener("click", function () { toggleImageUsage(option.id); }); usageChipById[option.id] = chip; usageChips.appendChild(chip); }); sec.appendChild(usageChips); sec.appendChild(usageNote); sec.appendChild(strategySub); var currentStrategyIndex = imageStrategyCandidateIndex(STATE.image_strategy); if (STATE.image_strategy && STATE.image_strategy.rendering === "custom" && customCard) { selectCustomImageStrategy(); } else if (STATE.image_strategy && STATE.image_strategy.rendering === "custom") { presetSelect.value = ""; } else if (currentStrategyIndex >= 0) { presetSelect.value = String(currentStrategyIndex); if (customCard) customCard.classList.remove("selected"); syncCustomStrategy(false); } else if ((!STATE.image_strategy || !STATE.image_strategy.rendering) && needsGeneratedImagesForUsage(STATE.image_usage) && hasRecommendedStrategies && strategyCands.length) { selectImageStrategy(imageStrategySelectedIndex()); } refreshUsageChips(); host.appendChild(sec); } function renderImageProduction(host) { var sec = section("P", "sec_image_production", t("image_production_hint")); var body = el("div", "image-production-body"); sec.appendChild(body); refreshImageProduction = function () { body.innerHTML = ""; var summary = el("div", "subfield"); summary.appendChild(el("div", "subfield-label", t("image_source_summary"))); var chips = el("div", "chips locked-summary-chips"); (STATE.image_usage || []).forEach(function (id) { var option = findCatalogOption(CAT.image_usage, id); chips.appendChild(el("div", "chip selected locked-summary-chip", option ? optionLabel(option) : humanizeId(id))); }); summary.appendChild(chips); body.appendChild(summary); if (needsGeneratedImagesForUsage(STATE.image_usage)) { var pathField = el("div", "subfield"); pathField.appendChild(el("div", "subfield-label", t("image_ai_path"))); enumField(pathField, CAT.image_ai_path, recOrFirst("image_ai_path", CAT.image_ai_path), function () { return STATE.image_ai_path; }, function (value) { STATE.image_ai_path = value; }); body.appendChild(pathField); } }; refreshImageProduction(); host.appendChild(sec); } function renderMode(host) { var sec = section("M", "sec_mode"); function refresh() { setSectionNote(sec, STATE.generation_mode === "split" ? t("mode_split_desc") : t("mode_continuous_desc")); } enumField(sec, CAT.generation_mode, recOrFirst("generation_mode", CAT.generation_mode), function () { return STATE.generation_mode; }, function (v) { STATE.generation_mode = v; refresh(); }); refresh(); host.appendChild(sec); } function renderProactiveExecution(host) { var sec = section("A", "sec_proactive_execution", t("proactive_execution_hint")); var opts = [{ id: "off", label: t("off_default") }, { id: "on", label: t("on") }]; function addToggle(labelKey, descKey, stateKey, fallback) { var field = el("div", "subfield"); field.appendChild(el("div", "subfield-label", t(labelKey))); field.appendChild(el("div", "toggle-desc", t(descKey))); var recommended = booleanRecommendation(stateKey, fallback); enumField(field, opts, recommended ? "on" : "off", function () { return STATE[stateKey] ? "on" : "off"; }, function (value) { STATE[stateKey] = value === "on"; }); sec.appendChild(field); } addToggle( "proactive_speaker_notes", "proactive_speaker_notes_desc", "proactive_speaker_notes", true ); addToggle( "proactive_custom_animations", "proactive_custom_animations_desc", "proactive_custom_animations", false ); addToggle( "proactive_narration_audio", "proactive_narration_audio_desc", "proactive_narration_audio", false ); host.appendChild(sec); } function renderRefine(host) { var sec = section("R", "sec_refine"); var opts = [{ id: "off", label: t("off_default") }, { id: "on", label: t("on") }]; function refresh() { setSectionNote(sec, STATE.refine_spec ? t("refine_on_desc") : t("refine_off_desc")); } enumField(sec, opts, STATE.refine_spec ? "on" : "off", function () { return STATE.refine_spec ? "on" : "off"; }, function (v) { STATE.refine_spec = (v === "on"); refresh(); }); refresh(); host.appendChild(sec); } // Two-stage confirmation: communication contract, then complete final plan. var STAGE = 1; function stageNumber(data) { var raw = data && data.stage; raw = String(raw == null ? "" : raw).toLowerCase(); if (raw === "stage1") return 1; if (raw === "stage2") return 2; return 0; } function stageTitle(stage) { if (stage === 1) return t("stage_anchors"); if (stage === 2) return t("stage_final_plan"); return t("page_title"); } function renderForStage(stage) { var host = document.getElementById("sections"); host.innerHTML = ""; _secCounter = 0; var heading = document.querySelector("#topbar .topbar-titles h1"); if (heading) heading.textContent = stageTitle(stage); var hint = document.getElementById("topbar-hint"); if (hint) hint.textContent = stage === 1 ? t("stage1_current_value_hint") : t("topbar_hint"); // Detach the previous preview's repaint closures before the sections // re-render: color/typography auto-select would otherwise call them and // write to now-detached nodes until renderStylePreview remounts them. refreshStylePreview = function () {}; refreshImageStrategyPreview = function () {}; refreshImageProduction = function () {}; refreshBodySizeHint = function () {}; refreshSizeInputs = function () {}; DIRECTION_COMPONENT_PAINTERS = []; var previewHost = document.getElementById("topbar-preview"); if (previewHost) previewHost.innerHTML = ""; if (stage === 1) { // Stage 1 closes the communication contract and its design basis in // one submission. Detailed template controls stay hidden until the // user explicitly chooses template-backed design. renderCommunication(host); renderDelivery(host); renderCanvas(host); renderTemplateSelection(host); } else if (stage === 2) { if (previewHost) renderStylePreview(previewHost); if (previewHost) renderImageStrategyPreview(previewHost); // Stage 2 confirms one coherent deck solution. Bundles provide a // coordinated starting point; individual controls remain editable. renderTemplateApplication(host); renderDesignDirections(host); renderNarrativeDirection(host); renderVisualDirection(host); renderReadingMode(host); renderPages(host); var styleGroup = el("div", "style-group"); renderColor(styleGroup); renderIcons(styleGroup); renderTypography(styleGroup); host.appendChild(styleGroup); renderImageDirection(host); renderImageProduction(host); renderFormulaPolicy(host); renderProactiveExecution(host); renderMode(host); renderRefine(host); var refreshDirectionIndicators = function () { window.setTimeout(function () { refreshDesignDirectionState(); refreshDirectionComponentStates(); }, 0); }; host.oninput = refreshDirectionIndicators; host.onchange = refreshDirectionIndicators; host.onclick = refreshDirectionIndicators; } updateActionBar(stage); } function renderAll() { renderForStage(STAGE); } function updateActionBar(stage) { var btn = document.getElementById("btn-confirm"); btn.disabled = false; if (stage === 1) btn.textContent = t("btn_confirm_contract"); else if (stage === 2) btn.textContent = t("btn_confirm_final_plan"); else btn.textContent = t("btn_confirm"); } // ---- state init (once) ---------------------------------------------- function firstId(list) { if (!list || !list.length) return undefined; if (list[0].items) return (list[0].items[0] || {}).id; return list[0].id; } function pick(field, catList) { var recommended = recId(field); if (recommended == null || recommended === "") { recommended = normalizeRecId(field, directionField(field)); } return recommended != null && recommended !== "" ? recommended : firstId(catList); } function initCreativeSelection(field, catalog, behaviorKey) { var value = pick(field, catalog); var behavior = customCandidateBehavior(field); if (value && value !== "custom" && !findCatalogOption(catalog, value)) { behavior = String(value); value = "custom"; } STATE[field] = value; STATE[behaviorKey] = behavior; } function initStage1State() { STATE.primary_language = recommendationLanguage(); STATE.canvas = pick("canvas", CAT.canvas); STATE.audience = (REC.audience && REC.audience.value) || ""; STATE.communication_intent = (REC.communication_intent && REC.communication_intent.value) || ""; STATE.audience_outcome = (REC.audience_outcome && REC.audience_outcome.value) || ""; STATE.core_message = (REC.core_message && REC.core_message.value) || ""; STATE.delivery_context = (REC.delivery_context && REC.delivery_context.value) || ""; STATE.artifact_afterlife = (REC.artifact_afterlife && REC.artifact_afterlife.value) || ""; STATE.content_divergence = (REC.content_divergence && REC.content_divergence.value) || ""; // free text; blank = balanced default } // Stage-2 fields are (re-)read from the recommendations. At boot they come from // the active stage file; after a stage-1 confirm enterStage() calls this again // with the newly authored candidates. Stage-1 STATE is preserved // across the single-session transition — this never resets the contract. function initStage2State() { [ "audience", "communication_intent", "audience_outcome", "core_message", "delivery_context", "artifact_afterlife", "content_divergence" ].forEach(function (field) { var value = recValue(field); if (value && typeof value === "object") value = value.value; if (value != null) STATE[field] = String(value); }); STATE.primary_language = recommendationLanguage(); resetTypographySizeOverrides(); var templateApplication = templateApplicationRecommendation(); if (templateApplication != null) { STATE.template_application = templateApplication; } else if (stageNumber(REC) === 2) { delete STATE.template_application; } // Reading mode is a design-density tool, not part of the communication // purpose. Keep the legacy delivery_purpose key for JSON compatibility. STATE.delivery_purpose = recId("delivery_purpose") || directionField("delivery_purpose") || STATE.delivery_purpose || "balanced"; STATE.page_count = (REC.page_count && REC.page_count.value != null) ? String(REC.page_count.value) : (STATE.page_count || ""); initCreativeSelection("mode", CAT.modes, "mode_behavior"); initCreativeSelection("visual_style", CAT.visual_styles, "visual_style_behavior"); var cc = colorRecommendationCandidates(); var csel = (REC.color && REC.color.selected != null) ? REC.color.selected : selectedDesignDirectionIndex(); var c0 = cc[Math.min(csel, Math.max(cc.length - 1, 0))] || {}; STATE.color = { name: localized(c0, "name") || c0.name || "", palette: Object.assign({}, normPalette(c0)) }; STATE.icons = pick("icons", CAT.icons); var tc = typographyRecommendationCandidates(); var tsel = (REC.typography && REC.typography.selected != null) ? REC.typography.selected : selectedDesignDirectionIndex(); var t0 = normTypography(tc[Math.min(tsel, Math.max(tc.length - 1, 0))] || {}); STATE.typography = { name: localized(t0, "name") || t0.name || "", heading: t0.heading || {}, body: t0.body || {}, body_size: t0.body_size || typographyBodySize(REC.typography), sizes: Object.assign({}, t0.sizes || {}) }; if (t0.custom) STATE.typography.custom = t0.custom; // Guarantee a body baseline even when a candidate omitted body_size, on // any canvas (PPT → px default by purpose, non-PPT → px from effective span), // so role sizes never derive from an empty anchor. if (STATE.typography && !STATE.typography.body_size) { STATE.typography.body_size = defaultBodySizeForCanvas(STATE.canvas, STATE.delivery_purpose); } // Preserve an authored positive candidate anchor; derive only the roles // that remain unpinned. A user reading-mode change may reset it later. if (stageNumber(REC) === 2) syncUnpinnedTypographySizes(false); var rawImageUsage = recValue("image_usage"); STATE.image_usage = selectedImageUsageIds(rawImageUsage); if (!STATE.image_usage.length) { STATE.image_usage = [defaultImageUsageId()]; } STATE.image_notes = imageUsageNotesRecommendation(rawImageUsage); STATE.image_strategy_custom = imageStrategyCustomCandidate(); var strategyCandidates = imageStrategyRecommendationCandidates(); var directionStrategy = directionField("image_strategy"); var customStrategyRecommended = recId("image_strategy") === "custom"; if (customStrategyRecommended && STATE.image_strategy_custom) { STATE.image_strategy = normalizedImageStrategy(STATE.image_strategy_custom); } else if (directionStrategy) { STATE.image_strategy = normalizedImageStrategy(directionStrategy); } else if (strategyCandidates.length) { STATE.image_strategy = normalizedImageStrategy( strategyCandidates[imageStrategySelectedIndex()] || strategyCandidates[0] ); } var directions = designDirectionCandidates(); if (directions.length) { var selected = selectedDesignDirectionIndex(); applyDesignDirection(directions[selected], selected, false); } } function initProductionState() { STATE.formula_policy = pick("formula_policy", CAT.formula_policy); STATE.image_ai_path = pick("image_ai_path", CAT.image_ai_path); STATE.proactive_speaker_notes = booleanRecommendation("proactive_speaker_notes", true); STATE.proactive_custom_animations = booleanRecommendation("proactive_custom_animations", false); STATE.proactive_narration_audio = booleanRecommendation("proactive_narration_audio", false); STATE.generation_mode = pick("generation_mode", CAT.generation_mode); STATE.refine_spec = !!((REC.refine_spec && REC.refine_spec.value) || (REC.recommend && REC.recommend.refine_spec)); } function initState() { initStage1State(); } // ---- confirm + close ------------------------------------------------- function showConfirmedOverlay() { var ov = document.getElementById("confirmed-overlay"); ov.querySelector(".cf-title").textContent = t("confirmed_title"); ov.querySelector(".cf-hint").textContent = t("confirmed_hint"); ov.style.display = "flex"; } // ---- staged submit + next-stage transitions ------------------------- function communicationPayload() { var payload = { canvas: STATE.canvas, audience: STATE.audience, communication_intent: STATE.communication_intent, audience_outcome: STATE.audience_outcome, core_message: STATE.core_message, delivery_context: STATE.delivery_context, artifact_afterlife: STATE.artifact_afterlife, content_divergence: STATE.content_divergence }; if (STATE.primary_language && STATE.primary_language !== "und") { payload.primary_language = STATE.primary_language; } return payload; } function stage1Payload() { var payload = communicationPayload(); payload.stage = "stage1"; payload.template_selection = { mode: TEMPLATE_MODE, selection_keys: TEMPLATE_MODE === "templates" ? TEMPLATE_SELECTED_KEYS.slice() : [] }; return payload; } function normalizeCreativePayload(payload) { if (payload.mode !== "custom") delete payload.mode_behavior; if (payload.visual_style !== "custom") delete payload.visual_style_behavior; var imageStrategy = payload.image_strategy; if (imageStrategy && imageStrategy.rendering === "custom") { if (!imageStrategy.behavior && imageStrategy.custom) { imageStrategy.behavior = imageStrategy.custom; } delete imageStrategy.custom; } else if (imageStrategy) { delete imageStrategy.behavior; delete imageStrategy.custom; } delete payload.image_strategy_custom; return payload; } function customSelectionsValid(payload) { var imageStrategy = payload.image_strategy || {}; var valid = payload.mode !== "custom" || String(payload.mode_behavior || "").trim(); valid = valid && (payload.visual_style !== "custom" || String(payload.visual_style_behavior || "").trim()); valid = valid && (imageStrategy.rendering !== "custom" || String(imageStrategy.behavior || "").trim()); if (!valid) { document.getElementById("confirm-status").textContent = t("custom_behavior_required"); } return !!valid; } function imageStrategyValid(payload) { if (!needsGeneratedImagesForUsage(payload.image_usage)) return true; var imageStrategy = payload.image_strategy || {}; var rendering = String(imageStrategy.rendering || "").trim(); if (!rendering) { document.getElementById("confirm-status").textContent = t("image_strategy_required"); return false; } var presetIds = imageStrategyCatalogCandidates().map(function (candidate) { return candidate.rendering; }); if (rendering !== "custom" && presetIds.length && presetIds.indexOf(rendering) < 0) { document.getElementById("confirm-status").textContent = t("image_strategy_invalid"); return false; } return true; } function positiveNumber(value) { var number = parseFloat(value); return isFinite(number) && number > 0; } function designSystemValid(payload) { var color = payload.color || {}; var palette = color.palette || {}; var completePalette = PALETTE_ROLES.every(function (role) { return !!normHex(palette[role]); }); var customPalette = color.name === "custom" && String(color.custom || "").trim(); var typography = payload.typography || {}; var completeFamilies = typographyFamiliesComplete(typography); var sizes = typography.sizes || {}; var completeSizes = positiveNumber(typography.body_size) && ["title", "subtitle", "annotation"].every(function (role) { return positiveNumber(sizes[role]); }); var valid = (completePalette || customPalette) && completeFamilies && completeSizes; if (!valid) { var message = color.name === "custom" && !customPalette ? t("custom_color_required") : t("design_system_required"); document.getElementById("confirm-status").textContent = message; } return !!valid; } function submitStage(payload, nextStage) { var btn = document.getElementById("btn-confirm"); btn.disabled = true; fetch("/api/confirm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function (r) { if (!r.ok) throw new Error("stage submit failed"); showDeriving(); pollForStage(nextStage); }).catch(function () { btn.disabled = false; document.getElementById("confirm-status").textContent = t("error_retry"); }); } function imageUsageValid(value) { var ids = selectedImageUsageIds(value); if (!ids.length) { document.getElementById("confirm-status").textContent = t("image_usage_required"); return false; } if (ids.indexOf("none") >= 0 && ids.length > 1) { document.getElementById("confirm-status").textContent = t("image_usage_none_exclusive"); return false; } return true; } function templateSelectionValid() { var valid = TEMPLATE_MODE === "free_design" || (TEMPLATE_MODE === "templates" && TEMPLATE_SELECTED_KEYS.length > 0); if (!valid) { document.getElementById("confirm-status").textContent = t("template_selection_required"); } return valid; } function submitStage1() { if (!templateSelectionValid()) return; submitStage(stage1Payload(), 2); } function submitStage2() { confirm(); } function showDeriving() { document.getElementById("sections").style.display = "none"; document.getElementById("actionbar").style.display = "none"; var l = document.getElementById("loading"); l.textContent = t("deriving"); l.style.display = "block"; } // Poll session state first. It is derived from recommendation stage files // and result.json, so a recovered server can tell the existing page exactly when // the next stage is ready. function pollForStage(nextStage) { fetchJson("/api/session", "session") .then(function (session) { var readyStage = Number(session && session.recommendation_stage_number || 0); if (readyStage < nextStage) { setTimeout(function () { pollForStage(nextStage); }, 1200); return null; } return fetchJson("/api/recommendations", "recommendations").then(function (data) { var serverStage = stageNumber(data); if (data && typeof serverStage === "number" && serverStage >= nextStage) { enterStage(data, serverStage); } else { setTimeout(function () { pollForStage(nextStage); }, 1200); } return null; }); }).catch(function (err) { var l = document.getElementById("loading"); if (l) l.textContent = t("connection_lost") + " " + (err && err.message ? err.message : ""); setTimeout(function () { pollForStage(nextStage); }, 1500); }); } function enterStage(data, stage) { REC = data; if (stage === 2) { initStage2State(); initProductionState(); } STAGE = stage; document.getElementById("loading").style.display = "none"; document.getElementById("sections").style.display = "block"; document.getElementById("actionbar").style.display = "flex"; document.getElementById("confirm-status").textContent = ""; renderForStage(stage); } function confirm() { var btn = document.getElementById("btn-confirm"); var payload = JSON.parse(JSON.stringify(STATE)); normalizeTypographyForSubmit(payload); payload.stage = "final"; payload.image_usage = selectedImageUsageIds(payload.image_usage); if (!imageUsageValid(payload.image_usage)) return; if (!String(payload.image_notes || "").trim()) delete payload.image_notes; if (!needsGeneratedImagesForUsage(payload.image_usage)) { delete payload.image_ai_path; delete payload.image_strategy; } else { payload.image_strategy = normalizedImageStrategy(payload.image_strategy); } normalizeCreativePayload(payload); if (!imageStrategyValid(payload)) return; if (!designSystemValid(payload)) return; if (!customSelectionsValid(payload)) return; btn.disabled = true; fetch("/api/confirm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function (r) { if (!r.ok) throw new Error("confirm failed"); showConfirmedOverlay(); fetch("/api/shutdown", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason: "confirmed" }) }).catch(function () { /* server already gone — fine */ }); setTimeout(function () { try { window.close(); } catch (e) { /* ignore */ } }, 400); }).catch(function () { btn.disabled = false; document.getElementById("confirm-status").textContent = t("error_retry"); }); } // ---- boot ------------------------------------------------------------ function showError(msg) { document.getElementById("loading").style.display = "none"; var e = document.getElementById("error"); e.style.display = "block"; e.textContent = msg; } function fetchJson(url, label) { return fetch(url, { cache: "no-store" }).then(function (r) { return r.text().then(function (text) { var data = null; if (text) { try { data = JSON.parse(text); } catch (e) { if (r.ok) throw new Error((label || url) + ": invalid JSON"); } } if (!r.ok) { var serverMsg = data && data.error ? data.error : (text || r.statusText || r.status); throw new Error((label || url) + ": " + serverMsg); } return data || {}; }); }); } function loadCatalogs() { return fetchJson("/api/catalogs", "catalogs") .catch(function () { return fetchJson("/static/catalogs.json", "static catalogs"); }); } function loadIconPreviews() { return fetchJson("/api/icon-previews", "icon previews") .catch(function () { return {}; }); } function loadAiImageComparison() { return fetchJson("/api/ai-image-comparison", "AI image comparison") .catch(function () { return {}; }); } function applyServerLanguage(data) { var requested = data && data.lang; if (requested !== "zh" && requested !== "en" && requested !== "ja" && requested !== "zh-TW") return; var hasStored = false; try { hasStored = !!window.localStorage.getItem("ppt_lang"); } catch (e) { /* ignore */ } if (hasStored) return; LANG = requested; applyStaticTranslations(); var toggleBtn = document.getElementById("btn-lang-toggle"); if (toggleBtn) refreshLangToggle(toggleBtn); } function loadStrategistUi(forceStage) { return Promise.all([ loadCatalogs(), fetchJson("/api/recommendations", "recommendations"), loadIconPreviews(), loadAiImageComparison() ]).then(function (res) { CAT = res[0]; REC = res[1]; ICON_PREVIEWS = res[2] || {}; AI_IMAGE_COMPARISON = res[3] || {}; applyServerLanguage(REC); var activeStage = forceStage || stageNumber(REC); if (activeStage === 1) { if (!REC.template_options || typeof REC.template_options !== "object" || Array.isArray(REC.template_options)) { throw new Error("Stage 1 recommendations must include template_options"); } initTemplateOptions(REC.template_options); } initState(); enterStage(REC, activeStage); return REC; }); } function boot() { applyStaticTranslations(); var toggleBtn = document.getElementById("btn-lang-toggle"); var langMenu = document.getElementById("lang-menu"); refreshLangToggle(toggleBtn); var setMenuOpen = function (open) { langMenu.hidden = !open; toggleBtn.setAttribute("aria-expanded", open ? "true" : "false"); if (open) { var sel = langMenu.querySelector("li.selected") || langMenu.querySelector("li[data-lang]"); if (sel) sel.focus(); } }; var chooseLang = function (v) { setMenuOpen(false); toggleBtn.focus(); if (v !== "ja" && v !== "en" && v !== "zh" && v !== "zh-TW") return; if (v === LANG) return; LANG = v; try { window.localStorage.setItem("ppt_lang", LANG); } catch (e2) { /* ignore */ } applyStaticTranslations(); refreshLangToggle(toggleBtn); if (REC && CAT) { renderAll(); // STATE persists → selections preserved } }; toggleBtn.addEventListener("click", function (e) { e.stopPropagation(); setMenuOpen(langMenu.hidden); }); toggleBtn.addEventListener("keydown", function (e) { if (e.key === "Escape" && !langMenu.hidden) { e.stopPropagation(); setMenuOpen(false); } else if ((e.key === "ArrowDown" || e.key === "ArrowUp") && langMenu.hidden) { e.preventDefault(); e.stopPropagation(); setMenuOpen(true); } }); langMenu.addEventListener("click", function (e) { e.stopPropagation(); var li = e.target && e.target.closest ? e.target.closest("li[data-lang]") : null; if (li) chooseLang(li.getAttribute("data-lang")); else setMenuOpen(false); }); langMenu.addEventListener("keydown", function (e) { e.stopPropagation(); // page-level shortcuts must not fire while the menu is open var items = Array.prototype.slice.call(langMenu.querySelectorAll("li[data-lang]")); var idx = items.indexOf(document.activeElement); if (e.key === "Escape") { setMenuOpen(false); toggleBtn.focus(); } else if (e.key === "ArrowDown") { e.preventDefault(); (items[idx + 1] || items[0]).focus(); } else if (e.key === "ArrowUp") { e.preventDefault(); (items[idx - 1] || items[items.length - 1]).focus(); } else if (e.key === "Home") { e.preventDefault(); items[0].focus(); } else if (e.key === "End") { e.preventDefault(); items[items.length - 1].focus(); } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); } else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (idx >= 0) chooseLang(items[idx].getAttribute("data-lang")); } }); toggleBtn.parentElement.addEventListener("focusout", function (e) { if (!langMenu.hidden && !toggleBtn.parentElement.contains(e.relatedTarget)) setMenuOpen(false); }); document.addEventListener("click", function () { if (!langMenu.hidden) setMenuOpen(false); }); document.getElementById("btn-confirm").addEventListener("click", function () { if (STAGE === 1) submitStage1(); else if (STAGE === 2) submitStage2(); else confirm(); }); // Session remains the first network read so completed runs can close // cleanly. Active runs always load the current Strategist stage; Stage 1 // carries its template catalog inside the recommendation payload. fetchJson("/api/session", "session").catch(function () { return { phase: "strategist" }; }).then(function (session) { if (session && session.status === "done") { document.getElementById("loading").style.display = "none"; showConfirmedOverlay(); return null; } return loadStrategistUi().catch(function (err) { showError(t("load_error") + " " + (err && err.message ? err.message : "")); return null; }); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();