概要
既製のAIチャットアプリについて、利用言語をPython(Streamlit)からNext.jsに置き換えようとした際に起こった事象についてまとめたもの
(現在も着手中のため、適宜追加します)
前提
Next.js(というよりフロントエンド全般)の勉強歴が2週間+AIにガンガン質問して実装をする形式のため、かなり初歩的な内容になっています
言いたいこと
作りこみ方法はAppルータ形式とPagesルータ形式の2種類があるが、そのうちの前者はまだ発展途上
GPTのバージョンが古いとしれっと後者の実装を回答してくる(しかも正しいかのように)ので、騙されないようにしよう!
前者についてChat-GPTに聞くならGPT-4oが良い
Web検索で見つかるサイトも今のところ(2024/5/17時点)は後者が多い
本文
1.npx実行時にフォルダなしエラー
事象
コマンドプロンプトでnpxを実行しプロジェクトを立ち上げようとした際に発生。「指定した場所にnpmフォルダが見つからない」エラーになる
対策
エラーで指定された場所にnpmという名前のフォルダを配置する(空フォルダでよい)
2.プログラム実行時にmoduleが見つからずエラー
事象
npm run devで作成したプログラムを実行した際に発生
ガイド通りに進めたがlocalhostアクセス時に「moduleが見つからない」エラーが発生する
対策
「#」がパスに含まれていると発生
今回のケースだとデスクトップ(C:\Users\#User0123\Desktop\Project)に配置したため
実行するプロジェクトの配置パスを「#」を含まない場所に配置すればよい
3.runした際に設定したファビコンが出ない
事象
Appルータ形式で作りこみをしたプログラムを実行したとき、appフォルダに配置したファビコンが出てこなかった
対策
Appフォルダ配下のみにおかないと反応しない
調査作業の時に「publicフォルダに配置することで出力される」との内容があったため取り込んだが、それはPagesルータの対策法だった
npxで作ったばかりのプログラムなら正しく動くので余計なことしなければOK
(PagesルータとAppルータという2種類の方法があり、対処法がそれぞれ違うので混同しないように)
4.「暗黙的にanyになります」となってコンパイルエラー
事象
TypeScriptを利用する場合において、JavaScriptのサンプルプログラムを持ってきた場合に、上記メッセージが出てコンパイルが通らない
対策
TypeScriptは型宣言が必要になるため、適切な型を宣言するか、anyとして宣言すればよい(後者の場合は型チェックが出来るというTypeScriptとしての強みがなくなるが、機能実行を優先する場合は検討する)
5.ChatGPT出力結果のストリーム表示をしたい
事象
ChatGPTを利用するためのAPIについて、デフォルトの出力結果はまとめて出力である
これをリアルタイムで出力するようにしたい
対策
API部品の呼び出しをする際に出力部分のstream設定をtrueとすればリアルタイムで情報が取得される
ただし受け取った情報は細切れになるので出力側が制御する必要あり
6.MarkdownをHTMLに変換する際にエラー
事象
ChatGPTからの返却がMarkdown形式で返ってくるため、これをHTMLで描写するためにmarkedパッケージのimportを実施した
プログラム実行時に、変換(parse)メソッドを使おうとした際にundefinedが出て読み取れないエラーが出る
対策
import文を以下の通り修正する
修正前:import marked from 'marked';
修正後:import {marked} from 'marked';
7.DB取得用のAPIを実行しようとすると404エラーが発生する
事象
Appルータ形式でAPI実行する内容を作成したが
実行されなかった。内容を追う限りだと404エラーが発生しているようだった
呼び出し元 app/page.tsx
// DBアクセス処理を追加
// resにはDBから取得したデータが入る
// 以下はサーバーから返されたデータをコンソールに表示する例(idを引数として渡す)
const response = await fetch('/api/query/DBAccess?id='+inputValue)
const data = await response.json();
呼び出し先 app/api/query/DBAccess.ts
// サンプルのためHello,worldを詰めて返却するだけになっている
export async function GET(request: any) {
return new Response(JSON.stringify({ message: 'Hello, world!' }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
}
ChatGPTにも確認したが、フォルダ体系には問題がないような回答がなされていた
対策
Appルータ形式はAPIフォルダにおけるファイル名が決まっている
「route.js」または「route.ts」にしなければならなかった
呼び出し先を「route.ts」に修正したうえで
呼び出し元のパス指定を以下の通り修正すればよい。
// DBアクセス処理を追加
// resにはDBから取得したデータが入る
// 以下はサーバーから返されたデータをコンソールに表示する例(idを引数として渡す)
const response = await fetch('/api/query?id='+inputValue)
const data = await response.json();
8.DB取得用のAPIを実行すると結果をreturnする際にエラーになる【5/17 追加】
事象
SQLを発行してローカルのMySQLと連携するAPIを作成した際に発生。
「TypeError: res.status is not a function」というエラー文言が出力される。
'use server';
import { NextApiRequest, NextApiResponse } from 'next';
import mysql from 'mysql2/promise';
export async function GET(req: NextApiRequest ,res: NextApiResponse) {
//中略
const [results] = await connection.query('SELECT * FROM sample');
if (!results) {
console.log('No data found');
return res.status(404).json({ message: 'No data found' });
} else {
console.log('Fetched data:', results);
return res.status(200).json(results);
}
//中略
}
対策
こちらもルータ形式によって実装が異なるために発生した事象。
Next.jsのappディレクトリを使用している場合、APIルートのハンドラはNextApiRequestやNextApiResponseを直接受け取るのではなく、RequestとResponseオブジェクトを使用している。
Appルータ形式は「NextRequest(Response)」による利用を想定されているため、「NextAPIRequest(Response)」を使わずに実装する。
逆にPagesルータ形式の場合については「NextApiRequest(Response)」を利用して実装する。
'use server';
import { NextRequest, NextResponse } from 'next/server';
import mysql from 'mysql2/promise';
export async function GET(req: NextRequest) {
//中略
const [results] = await connection.query('SELECT * FROM sample');
if (!results) {
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
}
9.DBレコード取得後の件数判定でエラーになる【5/20 追加】
事象
SQLを発行してレコード取得を行い、件数判定を行う処理で発生。以下のようなエラーが発生する。
プロパティ 'length' は型 'QueryResult' に存在しません。
プロパティ 'length' は型 'OkPacket' に存在しません。
//中略
// sample_dbデータベースのsampleテーブルからIDが一致する要素を取得する
const [results] = await connection.query('SELECT * FROM sample WHERE userid = ?', userid);
// resultsが空の場合はエラーを返す
if (results.length === 0) {
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results.length);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
対策
型推論のエラー。
javaとは異なり型宣言をしていないため、resultsの型が何かを読み取れる範囲で読み取る形(型推論)となっている。
現時点の条件ではlengthメソッドを使える型であるかが確定できない。
以下の通り、配列であるかどうかのチェックを追加することで抑止が可能。
//中略
// sample_dbデータベースのsampleテーブルからIDが一致する要素を取得する
const [results] = await connection.query('SELECT * FROM sample WHERE userid = ?', userid);
if (!Array.isArray(results) || results.length === 0){
console.log('No data found');
return new NextResponse(JSON.stringify({ message: 'No data found '}),{status:404});
} else {
console.log('Fetched data:', results.length);
return new NextResponse(JSON.stringify(results),{status:200});
}
//中略
10.一度設定したパラメータが意図せずに更新される【5/20 追加】
事象
ワンタイムパスワードとして6桁のランダムな数値を生成してメール送信し、ユーザーの入力値と比較させる処理を実装しようとしていた。
しかし、実際に検証してみたところ、alertとして出力された時点のotpの値と、入力値との比較で用いられるotpの値がずれていてログインできなかった。
import React,{ useState } from 'react';
const [otp, setOTP] = useState('');
// ワンタイムパスワードの生成処理
const handleGenerateOTP = () => {
// ここでサーバーにIDとPWを送信し、ワンタイムパスワードを取得する処理を行う
// ランダムな6桁の数字(100000から999999までのランダムな数字)を生成している
const randomOTP = Math.floor(100000 + Math.random() * 900000).toString();
// 以下はサーバーから返されたワンタイムパスワードをセットする例
setOTP(randomOTP); // 仮のワンタイムパスワード
alert('送信します:'+sampleData[0].EMAIL+'に'+otp+'を送信します。')
sendmail(sampleData[0].EMAIL , otp);
};
const handleLogin = () => {
// パスワード入力後の判定処理
// TODO:ワンタイムパスワードとの比較処理にする
if (inputValue === otp) {
// ログイン処理
}
};
対策
useStateの編集仕様によるもの。
useState(ここではsetOTP)が非同期的に状態を更新するため、otpが更新される前にalertやsendmailが動く。これにより古い値が使われることになる。
(inputValueと比較する時点ではotpはrandomOTPの値が格納されているたえ不一致になる)
解決策としては、randomOTPを直接使ってメール送信の処理を行う。
const [otp, setOTP] = useState('');
// ワンタイムパスワードの生成処理
const handleGenerateOTP = () => {
// ランダムな6桁の数字(100000から999999までのランダムな数字)を生成している
const randomOTP = Math.floor(100000 + Math.random() * 900000).toString();
// サーバーから返されたワンタイムパスワードをセットする
setOTP(randomOTP); // 仮のワンタイムパスワード
alert('送信します:'+sampleData[0].EMAIL+'に'+randomOTP+'を送信します。')
sendmail(sampleData[0].EMAIL , randomOTP);
};
const handleLogin = () => {
// パスワード入力後の判定処理
// TODO:ワンタイムパスワードとの比較処理にする
if (inputValue === otp) {
// ログイン処理
}
};
この例に関わらずuseStateの反映に時間差があるため、値を入れたはずなのに反映されていない事例が続出する。
確実に制御順を整えたい場合はuseEffectを使ったトリガー処理の作り込みが必要。
11.NextAuthのパス誤りでエラー【5/22 追加】
事象
セッション管理のためにNextAuthを利用しようとした際に発生。
以下のようなエラーメッセージが発生した。
「Appルータ形式だからかな?」と思ってパス回りを試行錯誤するも改善しない。
[next-auth][error][MISSING_NEXTAUTH_API_ROUTE_ERROR]
https://next-auth.js.org/errors#missing_nextauth_api_route_error Cannot find [...nextauth].{js,ts} in `/pages/api/auth`.
Make sure the filename is written correctly.
MissingAPIRoute [MissingAPIRouteError]:
Cannot find [...nextauth].{js,ts} in `/pages/api/auth`
対策
フォルダ名の指定誤り。app/api/auth配下のフォルダ名を以下の通りすべきだった。
誤:[…nextauth]
正:[...nextauth]
外見上だけだと全く違いが判らないが、誤は「…」1文字で、正は「.」3文字である。
また、nextauth以外の名前も許容していない(nextAuth、NextAuthなどだとダメ)。
12.画像生成時にURL情報が取得できずにエラー【5/29 追加】
事象
入力した文字列から画像生成するためにopenai.createImageを利用した際に発生。
responseから生成された画像が配置されたURL情報を取得しようとしたが、
AIが提案した方式のままだと「dataはない」とコンパイルエラーになってしまう。
response.urlという項目があったので使ってみたが、画像は出力されなかった。(ログを見る限り生成はされているように見えた)
busboy.on('finish', async () => {
console.log('Busboy finished processing');
try {
let imageGeneratedUrl = '';
// 画像生成のための API 呼び出し
if (text.includes('generate an image')) {
console.log('Generating image');
const response = await openai.createImage({
prompt: text,
n: 1,
size: '1024x1024',
});
imageGeneratedUrl = response.data[0].url; //ここでエラーになる
}
resolve(NextResponse.json({ text, files, imageGeneratedUrl }));
} catch (error) {
console.error('Error:', error);
reject(new NextResponse('Error generating image', { status: 500 }));
}
});
対策
responseのjson部を一度定義してから(値が完全に設定されることを担保するためにawaitを付与)、そのjsonの中身を抜き出すようにする。
busboy.on('finish', async () => {
console.log('Busboy finished processing');
try {
let imageGeneratedUrl = '';
// 画像生成のための API 呼び出し
if (text.includes('generate an image')) {
console.log('Generating image');
const response = await openai.createImage({
prompt: text,
n: 1,
size: '1024x1024',
});
const responseData = await response.json(); // 追加
imageGeneratedUrl = responseData.data[0].url; // コンパイルOK
}
resolve(NextResponse.json({ text, files, imageGeneratedUrl }));
} catch (error) {
console.error('Error:', error);
reject(new NextResponse('Error generating image', { status: 500 }));
}
});