← GPT-AI-BANNER
CODE · 6.6 KB

src/manage-panel.jsx

Workspace snapshot · 09/04 13:35

import React, { useEffect, useState } from "react";
import "./manage-panel.css";

const formats = [
  ["download-cover", "ダウンロード資料サムネ", "1200 × 630"], ["blog", "ブログサムネ", "1200 × 630"], ["youtube", "YouTubeサムネ", "1280 × 720"],
  ["web-hero", "Webサイト メインビジュアル", "1600 × 600"], ["web-banner", "Webサイト バナー", "1200 × 400"], ["sns-square", "SNS 正方形投稿", "1080 × 1080"],
  ["sns-portrait", "SNS 縦長投稿", "1080 × 1350"], ["story", "ストーリー・縦型表紙", "1080 × 1920"], ["display-ad", "Web広告", "1200 × 628"],
];
const variables = ["serviceName", "target", "angle", "useCase", "purpose", "width", "height", "aspectRatio", "colorScheme", "taste", "directionPrompt", "main", "sub", "offer", "cta"];

function Editor({ label, help, value, rows = 10, onChange }) {
  return <label className="mp-editor"><span>{label}</span><textarea rows={rows} value={value || ""} onChange={(event) => onChange(event.target.value)}/>{help && <small>{help}</small>}</label>;
}

export default function ManagePanel() {
  const [auth, setAuth] = useState("checking");
  const [token, setToken] = useState("");
  const [settings, setSettings] = useState(null);
  const [status, setStatus] = useState("");
  const load = async () => {
    const response = await fetch("/api/manage-settings");
    if (response.status === 401) return setAuth("login");
    const data = await response.json();
    if (!response.ok) { setStatus(data.error || "設定を取得できませんでした"); return setAuth("error"); }
    setSettings(data); setAuth("ready");
  };
  useEffect(() => { load(); }, []);
  const login = async (event) => {
    event.preventDefault(); setStatus("認証中…");
    const response = await fetch("/api/manage-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }) });
    const data = await response.json();
    if (!response.ok) return setStatus(data.error || "認証できませんでした");
    setToken(""); setStatus(""); await load();
  };
  const save = async () => {
    setStatus("保存中…");
    const response = await fetch("/api/manage-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings) });
    const data = await response.json();
    if (!response.ok) return setStatus(data.error || "保存できませんでした");
    setSettings((current) => ({ ...current, version: data.version })); setStatus(`VERSION ${data.version} として保存しました。次回生成から反映されます。`);
  };
  const setDirection = (key, value) => setSettings((current) => ({ ...current, directionPrompts: { ...current.directionPrompts, [key]: value } }));
  const setFormat = (key, value) => setSettings((current) => ({ ...current, useCasePrompts: { ...current.useCasePrompts, [key]: value } }));

  if (auth !== "ready") return <main className="manage-shell login-shell"><div className="mp-noise"/><section className="manage-login"><a href="/" className="mp-back">← AI Banner Lab</a><span className="mp-kicker">PRIVATE CREATIVE CONTROL ROOM</span><h1>Prompt<br/><em>Studio.</em></h1><p>AI Banner Labの頭脳とアートディレクションを、ここで調整します。</p>{auth === "login" ? <form onSubmit={login}><label>管理Token<input type="password" autoComplete="current-password" value={token} onChange={(event) => setToken(event.target.value)} autoFocus/></label>{status && <p className="mp-auth-error">{status}</p>}<button>編集画面を開く →</button></form> : <div className="mp-loading">{status || "設定を読み込んでいます…"}</div>}</section></main>;

  return <main className="manage-shell"><header className="manage-head"><div><a href="/" className="mp-back">← AI Banner Lab</a><span className="mp-kicker">PRIVATE CREATIVE CONTROL ROOM</span><h1>Prompt Studio.</h1><p>生成品質を決める4層を、役割ごとに編集。</p></div><div className="mp-version">LIVE<br/><b>VERSION {settings.version}</b></div></header>
    <div className="mp-compose"><b>COMMON</b><i>+</i><b>DIRECTION</b><i>+</i><b>FORMAT</b><i>+</i><b>USER INPUT</b><i>→</i><strong>GPT-IMAGE</strong></div>
    <nav className="mp-nav"><a href="#strategy">01 戦略</a><a href="#common">02 共通画像</a><a href="#direction">03 表現方向</a><a href="#formats">04 用途別</a></nav>
    <section className="mp-content">
      <details id="strategy" className="mp-section" open><summary><span><b>01</b>戦略・コピー設計</span><small>ターゲット / 訴求 / ブランド方向 / コピー</small></summary><div className="mp-body"><Editor label="戦略生成のシステム指示" value={settings.strategyInstruction} rows={12} onChange={(value) => setSettings({ ...settings, strategyInstruction: value })} help="JSON形式・安全制約・資料のブランド方向抽出はシステム側で固定追加されます。"/></div></details>
      <details id="common" className="mp-section" open><summary><span><b>02</b>共通画像プロンプト</span><small>すべての画像生成に適用</small></summary><div className="mp-body"><Editor label="GPT-IMAGE 共通指示" value={settings.imagePromptTemplate} rows={22} onChange={(value) => setSettings({ ...settings, imagePromptTemplate: value })}/><div className="mp-tokens"><span>INSERT VARIABLES</span>{variables.map((item) => <code key={item}>{`{{${item}}}`}</code>)}</div></div></details>
      <details id="direction" className="mp-section"><summary><span><b>03</b>表現方向</span><small>人物写真 / 強コピー / シンプル</small></summary><div className="mp-body mp-stack">{["人物写真", "強コピー", "シンプル"].map((item) => <Editor key={item} label={item} value={settings.directionPrompts[item]} rows={9} onChange={(value) => setDirection(item, value)}/>)}</div></details>
      <details id="formats" className="mp-section"><summary><span><b>04</b>用途別アートディレクション</span><small>9 FORMATS</small></summary><div className="mp-body mp-formats">{formats.map(([id, label, size], index) => <details className="mp-format" key={id}><summary><span><b>{String(index + 1).padStart(2, "0")}</b>{label}</span><small>{size}</small></summary><Editor label={`${label}専用指示`} value={settings.useCasePrompts[id]} rows={10} onChange={(value) => setFormat(id, value)}/></details>)}</div></details>
    </section>
    <div className="mp-save"><p><b>{status || "編集内容は保存するまで本番へ反映されません。"}</b><span>固定保護: 認証情報 / 入力命令の無効化 / 禁止表現 / サイズ正規化</span></p><button onClick={save} disabled={status === "保存中…"}>変更を保存して反映 →</button></div>
  </main>;
}