← GPT-AI-BANNER
CODE · 4.0 KB

src/engine.js

Workspace snapshot · 09/04 13:35

const stopWords = new Set(["サービス", "こと", "ため", "できる", "する", "提供", "向け", "から", "まで", "です", "ます", "利用", "ユーザー", "情報"]);

export function hash(text = "") {
  let value = 2166136261;
  for (const char of text) value = Math.imul(value ^ char.charCodeAt(0), 16777619);
  return value >>> 0;
}

export function sanitizeText(value, max = 4000) {
  return String(value || "").replace(/[\u0000-\u001f]/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
}

export function extractKeywords(input, limit = 6) {
  const text = sanitizeText(`${input.target || ""} ${input.summary || ""} ${input.documentText || ""}`, 8000);
  const pieces = text.match(/[一-龠々〆ヵヶァ-ヶー]{2,12}|[A-Za-z][A-Za-z0-9-]{2,20}/g) || [];
  const scored = new Map();
  pieces.forEach((word, index) => {
    if (stopWords.has(word)) return;
    scored.set(word, (scored.get(word) || 0) + 3 + Math.max(0, 8 - index) / 10);
  });
  return [...scored].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "ja")).slice(0, limit).map(([word]) => word);
}

export function makeTargets(input) {
  const name = sanitizeText(input.serviceName, 40) || "このサービス";
  const words = extractKeywords(input);
  const hint = sanitizeText(input.target, 50) || `${words[0] || "業務改善"}を担う少人数チーム`;
  const topic = words[1] || words[0] || "日々の業務";
  const outcome = words[2] || "成果";
  return [
    { name: hint, pain: `${topic}に時間を取られ、${outcome}に集中できない`, job: `${topic}の手戻りを減らす`, future: `${outcome}へすぐ動ける`, value: `${name}で${topic}を短縮` },
    { name: `${name}を比較検討する決裁者`, pain: `${name}の違いと導入価値を説明しにくい`, job: "関係者が判断できる材料を揃える", future: "導入後の変化を具体的に描ける", value: `${outcome}につながる価値を可視化` },
    { name: `${outcome}に責任を持つ現場リーダー`, pain: `${topic}が属人化し、打ち手が固定される`, job: "複数の仮説を同じ条件で比べる", future: "次の一手を根拠を持って選べる", value: `${name}で選択肢と判断速度を増やす` },
  ];
}

export function makeAngles(target) {
  return [
    { type: "ペイン", title: `${target.job}。その手戻りを減らす`, detail: target.pain },
    { type: "ベネフィット", title: target.future, detail: target.value },
    { type: "問いかけ", title: `${target.name}は、今のやり方で十分ですか?`, detail: `${target.value}という選択肢` },
    { type: "危機感", title: "迷っている間にも、機会は過ぎていく", detail: target.pain },
  ];
}

export function makeCopyCandidates(input, target, angle, round = 0) {
  const service = sanitizeText(input.serviceName, 32) || "このサービス";
  const words = extractKeywords(input);
  const topic = words[round % Math.max(words.length, 1)] || target.job;
  const mains = [angle.title, `${topic}を、${service}でもっと前へ。`, `${target.future}。${service}なら。`, `${target.pain.split(/[、。]/)[0]}を変える。`, `${topic}の次の一手を、今日決める。`];
  return [0, 1, 2].map((offset) => ({ id: `${hash(`${service}${angle.type}${round}${offset}`)}`, main: mains[(round + offset) % mains.length], sub: `${target.name}へ。${target.value}。`, offer: round % 2 ? `${service}のデモを試す` : "無料ベータで体験", cta: round % 2 ? "詳しく見る" : "今すぐ試す" }));
}

export function makeCopy(input, target, angle, variant = 0) {
  return makeCopyCandidates(input, target, angle, variant)[0];
}

export function shortenCopy(copy, max = 16) {
  const clean = copy.main.replace(/[。!!?]+$/g, "");
  return { ...copy, main: clean.length > max ? `${clean.slice(0, max)}。` : `${clean}。` };
}

export function strengthenCopy(copy) {
  const clean = copy.main.replace(/[。!!?]+$/g, "");
  return { ...copy, main: `${clean}、今すぐ。`, cta: copy.cta === "今すぐ試す" ? copy.cta : `今すぐ${copy.cta}` };
}