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>;
}