前回(第2回)でスコアリングと文章改善のロジックを実装しましたが、実際にいろいろな文章で試すうちに2つの大きな問題に直面しました。ひとつは「特定の文章でJSONパースが失敗する」こと、もうひとつは「スマホで見るとレイアウトが激しく崩れる」ことです。今回はこの2つのデバッグ記録を残します。

URLを含む文章でJSONパースが失敗する

テストに使っていた記事の中にhttps://github.com/?utm_source=chatgpt.comのようなURLが含まれていたところ、突然エラーが発生しました。

エラー:Expected ',' or '}' after property value in JSON
at position 96 (line 1 column 97)

原因を調べると、AIが生成するJSONのcommentフィールドの中に、文章中の引用符やURLのクエリパラメータがそのまま混入し、構文として壊れたJSONを返していたことが分かりました。当初は単純な正規表現でJSON部分を抜き出していただけだったため、こうした崩れに対して脆弱でした。

// 当初の脆弱な実装
var jm = raw.match(/\{[\s\S]*\}/);
var data = JSON.parse(jm[0]); // ← ここで例外が発生

5段階のJSON修復ロジックを実装

1回のパースが失敗しても即座にエラーにせず、複数の修復戦略を順番に試すsafeParseScore関数を実装しました。

function safeParseScore(raw) {
  if (!raw) return null;
  var start = raw.indexOf('{');
  var end = raw.lastIndexOf('}');
  if (start === -1 || end === -1) return extractByRegex(raw);
  var jsonStr = raw.slice(start, end + 1);

  // 1. そのままパース
  try { return JSON.parse(jsonStr); } catch(e) {}

  // 2. commentを空にして再パース
  try {
    var s2 = jsonStr.replace(
      /"comment"\s*:\s*"[\s\S]*?"(\s*[,}])/,
      '"comment":""$1'
    );
    return JSON.parse(s2);
  } catch(e) {}

  // 3. commentフィールド自体を削除して再パース
  try {
    var s3 = jsonStr.replace(
      /,\s*"comment"\s*:\s*"[\s\S]*?"(\s*\})/, '$1'
    );
    return JSON.parse(s3);
  } catch(e) {}

  // 4. 正規表現で数値キーだけ個別に抽出
  return extractByRegex(raw);
}

4段階目のextractByRegexでは、各スコア項目のキー名を目印に"tone":8のような数値部分だけを正規表現で拾い、JSON構造そのものが壊れていても診断結果自体は復元できるようにしました。これにより、ユーザー視点では「エラーで止まる」体験をほぼなくすことができました。

合わせて見直したプロンプト側の対策

「commentの文字列内にダブルクォート・バックスラッシュを含めないこと」とプロンプトに明記し、AI側にも崩れにくいJSONを生成するよう指示を追加しました。アプリ側の防御とプロンプト側の予防は両方必要だと感じています。

スマホ表示でレイアウトが崩壊する

PC画面ではきれいに表示されていたのに、スマートフォンで開くと「改善テキスト / 診断コメント」という見出し文字が縦に折り返されて読めなくなる現象が発生しました。原因は、PC向けに組んだ2カラムgridレイアウトを、画面幅が狭いスマホでもそのまま維持しようとしていたためです。

/* 問題があった当初のCSS(PC専用設計をそのまま流用) */
.toolbar {
  display: grid;
  grid-template-columns: 1fr 1fr; /* 狭い画面だと右側が極端に圧縮される */
}

この問題を機に、モバイルファーストの設計に作り直すことにしました。デフォルト(モバイル)では要素を縦1列に積み上げ、画面幅が641px以上になったときだけ2カラムに切り替えるアプローチです。

display:contentsで表示順だけを変える

レイアウトを直す過程で、もう一つ要望が出てきました。「スマホでは、改善前テキスト→改善前スコア→改善後テキスト→改善後スコアの順に並べたいが、PC画面のレイアウトは崩したくない」というものです。

HTMLの要素順序を変えずにこれを実現する鍵が、CSSのdisplay: contentsでした。これは親要素のボックスとしての性質を消し、子要素を直接祖先のレイアウトコンテキストに参加させるプロパティです。

@media (max-width: 640px) {
  .app { display: flex; flex-direction: column; }

  /* 親のgrid/flexを解除し、子をappの直接の子として扱う */
  .editor-area   { display: contents; }
  .score-section { display: contents; }

  /* orderプロパティで見た目の表示順だけを変える */
  .input-section  { order: 1; }
  .before-pane    { order: 2; }
  .output-section { order: 3; }
  .after-pane     { order: 4; }
}

これにより、HTMLのDOM構造はPCもスマホも完全に同一のまま、表示順だけをCSSで切り替えることができました。PC版のgridレイアウトには一切影響を与えない、安全な実装です。

高さを揃えるためのCSS変数設計

左の入力テキストエリアと右の出力エリアの高さがズレる問題も発生しました。左側にはボタン行・文字数行・進捗バーの3段があるのに対し、右側は単純なヘッダーのみだったことが原因です。

最終的には、ボタン行と出力ヘッダーを同じ1つのグリッド行として共通化し、文字数行と警告表示も同様に共通の行として扱う構成に変更しました。

/* ツールバーを左右共通の1行として実装 */
.toolbar {
  display: flex;
  align-items: center;
}
/* PC時のみ右側に出力ヘッダー相当の要素を追加表示 */
@media (min-width: 641px) {
  .toolbar-right {
    display: flex;
    border-left: 1px solid var(--border);
  }
}

左右で別々のヘッダーを作るのではなく、1つの行を共有する設計に変えたことで、高さのズレが構造的に発生しなくなりました。個別に高さを計算してJavaScriptで同期させる方法も試しましたが、最終的にはCSS設計そのものを見直す方が保守性が高いという結論に至っています。

3回にわたる開発記録は以上です。無料のGroq APIだけでも、プロンプト設計とフォールバック処理を丁寧に積み重ねれば、実用に耐えるツールを個人開発できることが分かりました。今後はサーバへのデプロイ後の運用状況も追って記事にしていく予定です。

FAQ

LLMが返すJSONが壊れるのはなぜですか?

文章中にURLや引用符が含まれていると、AIが生成するJSON内のcommentフィールドにダブルクォートやバックスラッシュが混入し、構文として不正なJSONになることがあります。プロンプトでの制約に加えて、アプリ側でも複数段階のフォールバック処理が必要です。

スマホでレイアウトが崩れる原因は何でしたか?

PC向けにgridで組んだ2カラムレイアウトをそのままモバイル幅に縮小すると、ヘッダーの文字が縦書きのように折り返されたり、要素の高さがチグハグになる問題が発生しました。モバイルファーストで設計し直し、CSSのdisplay:contentsとorderプロパティで表示順を制御することで解決しました。

display:contentsはどんな場面で役立ちますか?

親要素のボックスとしての性質を消し、子要素だけを直接の親のレイアウトコンテキストに参加させたい場合に有効です。HTMLの構造(DOM順序)を変えずに、CSSのorderプロパティだけで見た目の表示順を変更できます。