Following code is Astro+SolidJS+TypeScript+Firestore. Add UI to choose a room. Update messages if already exist in Firestore. Keep previously selected room in localStorage, default is "firstroom".
...
(ここは400行ほどのファイルを丸ごと貼り付けただけなので省略)
まず、部屋を選択するUIを追加するために、次のコードを追加してください。 (追記: 英語で指示したのに日本語で返事されちゃったのは「中高生のためのChatGPT」の動作テストのために自分で使ってみてて、システムプロンプトに「日本語で答えろ」という指示が入ってるせい。)
ChatRoom
インターフェースの下に、選択された部屋のIDを保持するためのシグナルを作成します。
const [selectedRoomId, setSelectedRoomId] = createSignal("");
onMount
関数内で、localStorage
から以前に選択された部屋を取得し、デフォルトの部屋を設定します。
onMount(() => {
// ...
const storedRoomId = localStorage.getItem("selectedRoomId") || "firstroom";
setSelectedRoomId(storedRoomId);
getOrCreateFirstRoom().then((roomId) => {
setSelectedRoomId(roomId);
restoreChatLog(roomId);
});
// ...
});
- 部屋を選択するためのドロップダウンリストを追加します。これを
<div my-6>
の直後に追加してください。
<select
value={selectedRoomId()}
onChange={(e) => {
setSelectedRoomId(e.currentTarget.value);
restoreChatLog(e.currentTarget.value);
localStorage.setItem("selectedRoomId", e.currentTarget.value);
}}
>
<option value="firstroom">firstroom</option>
{/* 他の部屋を追加する場合は、ここに<option>要素を追加してください */}
</select>
saveChatMessage
、restoreChatLog
、およびgetOrCreateFirstRoom
関数で使用されるハードコードされた"firstroom"
をselectedRoomId()
に置き換えます。
これで、部屋を選択するUIが追加され、選択された部屋のメッセージが更新されるようになりました。また、以前に選択された部屋が localStorage
に保存され、デフォルトの部屋が "firstroom" に設定されています。
コンパイルは通った。新規ルームを作るボタンをルーム一覧の隣につけて、ルーム一覧はFirestoreから取得するようにして。
新規ルームを作成するボタンを追加し、Firestoreからルーム一覧を取得するようにするには、以下の手順に従ってください。
- Firestoreからルーム一覧を取得するための関数を作成します。
async function fetchRooms(): Promise<ChatRoom[]> {
const userId = await signInAnonymously(auth);
const rooms: ChatRoom[] = [];
const querySnapshot = await db
.collection("users")
.doc(userId)
.collection("rooms")
.get();
querySnapshot.forEach((doc) => {
rooms.push({ id: doc.id, name: doc.data().name });
});
return rooms;
}
- ルーム一覧を保持するためのシグナルを作成します。
const [roomList, setRoomList] = createSignal<ChatRoom[]>([]);
onMount
関数内で、fetchRooms
関数を呼び出してルーム一覧を取得し、シグナルに設定します。
onMount(() => {
// ...
fetchRooms().then((rooms) => setRoomList(rooms));
// ...
});
- 新規ルームを作成する関数を作成します。
async function createNewRoom(roomName: string): Promise<string> {
const userId = await signInAnonymously(auth);
const roomRef = await db
.collection("users")
.doc(userId)
.collection("rooms")
.add({ name: roomName });
return roomRef.id;
}
- 新規ルームを作成するボタンを追加し、クリック時に新規ルームを作成し、ルーム一覧を更新します。これを
<select>
要素の後に追加してください。
<button
onClick={async () => {
const roomName = prompt("新しいルームの名前を入力してください");
if (roomName) {
const newRoomId = await createNewRoom(roomName);
const updatedRooms = await fetchRooms();
setRoomList(updatedRooms);
setSelectedRoomId(newRoomId);
restoreChatLog(newRoomId);
localStorage.setItem("selectedRoomId", newRoomId);
}
}}
>
新規ルームを作成
</button>
<select>
要素内の<option>
を、roomList
シグナルから動的に生成するように変更します。
<select
value={selectedRoomId()}
onChange={(e) => {
setSelectedRoomId(e.currentTarget.value);
restoreChatLog(e.currentTarget.value);
localStorage.setItem("selectedRoomId", e.currentTarget.value);
}}
>
<Index each={roomList()}>
{(room) => <option value={room().id}>{room().name}</option>}
</Index>
</select>
これで、新規ルームを作成するボタンが追加され、ルーム一覧がFirestoreから取得されるようになりました。新しいルームを作成すると、ルーム一覧が更新され、新しいルームが選択されます。
ボタンとルーム一覧のスタイルが他のUIと違ってダークモードにならないのだけど直して
ボタンとルーム一覧のスタイルを他のUIと同じようにダークモードに対応させるには、以下の手順に従ってください。
- 新規ルームを作成するボタンに
gen-slate-btn
クラスを追加します。
<button
onClick={async () => {
// ...
}}
gen-slate-btn
>
新規ルームを作成
</button>
<select>
要素にgen-slate-select
クラスを追加し、ダークモードに対応させるためのスタイルを適用します。<style>
タグをコンポーネントの最後に追加してください。
<style>
.gen-slate-select {
background-color: var(--slate-100);
color: var(--slate-800);
border: none;
padding: 4px 8px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.gen-slate-select:focus {
outline: none;
}
</style>
<select>
要素にgen-slate-select
クラスを追加します。
<select
value={selectedRoomId()}
onChange={(e) => {
// ...
}}
class="gen-slate-select"
>
<Index each={roomList()}>
{(room) => <option value={room().id}>{room().name}</option>}
</Index>
</select>
これで、新規ルームを作成するボタンとルーム一覧のスタイルが他のUIと同じようにダークモードに対応するようになりました。
すごいですね。バッチリです。細かい修正ですが、新規ルームを作成時にルーム名を聞く時、デフォルト値として今の日時を入れましょう。また、メッセージだけでなくSystemRoleもルームごとにFirestoreに保存すべきでした。
(追記: バッチリと書いてるが、2の指示に従うとエラーになるのでCSSに貼ってる。CSSファイルの存在はGPT4に教えてないからわからないのも仕方ない)
新規ルームを作成する際に、デフォルトのルーム名として現在の日時を設定し、SystemRoleもルームごとにFirestoreに保存するように変更するには、以下の手順に従ってください。
- 新規ルームを作成する際に、デフォルトのルーム名として現在の日時を設定します。
onClick
イベント内でprompt
関数を呼び出す前に、現在の日時を取得し、デフォルトのルーム名として設定します。
<button
onClick={async () => {
const currentDate = new Date();
const defaultRoomName = `${currentDate.getFullYear()}-${currentDate.getMonth() + 1}-${currentDate.getDate()} ${currentDate.getHours()}:${currentDate.getMinutes()}`;
const roomName = prompt("新しいルームの名前を入力してください", defaultRoomName);
// ...
}}
gen-slate-btn
>
新規ルームを作成
</button>
- SystemRoleをルームごとに保存するために、
saveChatMessage
関数を変更し、SystemRoleも引数として受け取ります。
async function saveChatMessage(roomId: string, chatMessage: ChatMessage, systemRole: string) {
const userId = await signInAnonymously(auth);
await db
.collection("users")
.doc(userId)
.collection("rooms")
.doc(roomId)
.collection("messages")
.add({
...chatMessage,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
await db
.collection("users")
.doc(userId)
.collection("rooms")
.doc(roomId)
.update({ systemRole });
}
sendMessage
関数内で、saveChatMessage
関数を呼び出す際に、currentSystemRoleSettings()
を引数として渡します。
const sendMessage = async (mode: string) => {
// ...
saveChatMessage(selectedRoomId(), m, currentSystemRoleSettings());
// ...
};
archiveCurrentMessage
関数内で、saveChatMessage
関数を呼び出す際に、currentSystemRoleSettings()
を引数として渡します。
const archiveCurrentMessage = () => {
// ...
saveChatMessage(selectedRoomId(), m, currentSystemRoleSettings());
// ...
};
これで、新規ルームを作成する際にデフォルトのルーム名として現在の日時が設定され、SystemRoleもルームごとにFirestoreに保存されるようになりました。
saveChatMessageでシステムロールを保存するのではなく、システムロールの変更時に保存するようにして
システムロールの変更時に保存するように変更するには、以下の手順に従ってください。
- システムロールを保存するための新しい関数を作成します。
async function saveSystemRole(roomId: string, systemRole: string) {
const userId = await signInAnonymously(auth);
await db
.collection("users")
.doc(userId)
.collection("rooms")
.doc(roomId)
.update({ systemRole });
}
SystemRoleSettings
コンポーネント内で、onBlur
イベントを追加し、システムロールが変更されたときにsaveSystemRole
関数を呼び出します。
<textarea
// ...
onBlur={() => saveSystemRole(selectedRoomId(), currentSystemRoleSettings())}
/>
saveChatMessage
関数からシステムロールの保存処理を削除します。
async function saveChatMessage(roomId: string, chatMessage: ChatMessage) {
const userId = await signInAnonymously(auth);
await db
.collection("users")
.doc(userId)
.collection("rooms")
.doc(roomId)
.collection("messages")
.add({
...chatMessage,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
}
sendMessage
関数とarchiveCurrentMessage
関数から、saveChatMessage
関数へのシステムロールの引数を削除します。
const sendMessage = async (mode: string) => {
// ...
saveChatMessage(selectedRoomId(), m);
// ...
};
const archiveCurrentMessage = () => {
// ...
saveChatMessage(selectedRoomId(), m);
// ...
};
これで、システムロールが変更されたときにFirestoreに保存されるようになりました。メッセージの送信時にはシステムロールが保存されなくなりました。
(追記: onBlurではなく保存ボタンを押した時に保存するようにした。コンポーネントの中身のことをGPT4は知らないのでボタンで保存するのではなくフォーカスを外すだけで自動保存にしてると思ったんだね。確かにその設計の方がいいかもなー。)
SystemRoleSettings コンポーネント内で、saveSystemRole 関数を呼び出すにはpropsに渡す必要がありますね、書いて。
おっしゃる通りです。SystemRoleSettings
コンポーネントに saveSystemRole
関数を渡すには、以下の手順に従ってください。
SystemRoleSettings
コンポーネントにsaveSystemRole
関数を渡すための新しいプロパティを追加します。
<SystemRoleSettings
// ...
saveSystemRole={saveSystemRole}
/>
SystemRoleSettings
コンポーネントの定義で、新しいプロパティを受け取ります。
interface SystemRoleSettingsProps {
// ...
saveSystemRole: (roomId: string, systemRole: string) => Promise<void>;
}
const SystemRoleSettings: Component<SystemRoleSettingsProps> = (props) => {
// ...
};
SystemRoleSettings
コンポーネント内で、onBlur
イベントでprops.saveSystemRole
関数を呼び出します。
<textarea
// ...
onBlur={() => props.saveSystemRole(selectedRoomId(), currentSystemRoleSettings())}
/>
これで、SystemRoleSettings
コンポーネント内で saveSystemRole
関数が呼び出されるようになりました。システムロールが変更されたときにFirestoreに保存されます。
ルームが変更された時にシステムロールをFirestoreから読むコードも必要ですね
ルームが変更されたときにシステムロールをFirestoreから読み込むには、以下の手順に従ってください。
- システムロールを取得するための新しい関数を作成します。
async function fetchSystemRole(roomId: string): Promise<string> {
const userId = await signInAnonymously(auth);
const roomDoc = await db
.collection("users")
.doc(userId)
.collection("rooms")
.doc(roomId)
.get();
return roomDoc.data()?.systemRole || DEFALUT_SYSTEM_ROLE;
}
<select>
要素のonChange
イベントで、fetchSystemRole
関数を呼び出し、システムロールを更新します。
<select
value={selectedRoomId()}
onChange={async (e) => {
setSelectedRoomId(e.currentTarget.value);
restoreChatLog(e.currentTarget.value);
localStorage.setItem("selectedRoomId", e.currentTarget.value);
const systemRole = await fetchSystemRole(e.currentTarget.value);
setCurrentSystemRoleSettings(systemRole);
}}
class="gen-slate-select"
>
<Index each={roomList()}>
{(room) => <option value={room().id}>{room().name}</option>}
</Index>
</select>
これで、ルームが変更されたときにシステムロールがFirestoreから読み込まれ、現在のシステムロールに設定されるようになりました。
良さそうです。さらに機能追加です。smoothToBottomを呼び出すかどうかを指定するチェックボックスをつけてください。また受信が完了した時にメロディを鳴らしてお知らせするようにしてください。
機能を追加するには、以下の手順に従ってください。
- smoothToBottomを呼び出すかどうかを指定するためのシグナルを作成します。
const [enableSmoothToBottom, setEnableSmoothToBottom] = createSignal(true);
- smoothToBottomを呼び出すかどうかを指定するチェックボックスを追加します。これを
<div>
要素の後に追加してください。
<label>
<input
type="checkbox"
checked={enableSmoothToBottom()}
onChange={(e) => setEnableSmoothToBottom(e.currentTarget.checked)}
/>
スクロールを自動で下へ
</label>
requestWithLatestMessage
関数内で、enableSmoothToBottom
シグナルの値に基づいてsmoothToBottom
を呼び出すかどうかを判断します。
if (enableSmoothToBottom()) {
smoothToBottom();
}
- 受信が完了したときにメロディを鳴らすために、
archiveCurrentMessage
関数内でAudio
オブジェクトを作成し、音声ファイルを再生します。音声ファイルはプロジェクト内に配置してください。
const archiveCurrentMessage = () => {
// ...
const audio = new Audio("/path/to/your/audio-file.mp3");
audio.play();
};
これで、smoothToBottomを呼び出すかどうかを指定するチェックボックスが追加され、受信が完了したときにメロディが鳴るようになりました。
audio自体を作って。
オーディオを作成するには、Web Audio APIを使用して、次の手順に従ってください。
- オーディオコンテキストを作成し、オシレーターとゲインノードを設定します。
function playMelody() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 0.01);
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
oscillator.frequency.value = 440;
oscillator.type = "sine";
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 1);
}
archiveCurrentMessage
関数内で、playMelody
関数を呼び出します。
const archiveCurrentMessage = () => {
// ...
playMelody();
};
これで、受信が完了したときにメロディが鳴るようになりました。このコードは、440Hzのサイン波を1秒間再生します。周波数や再生時間を変更することで、異なるメロディを作成できます。
もうちょっと工夫して作業が完了した感じのあるメロディを作曲して
作業が完了した感じのあるメロディを作成するには、以下の手順に従ってください。
- メロディを再生するための関数
playNote
を作成します。この関数は、指定された周波数と再生時間で音を再生します。
function playNote(audioContext, frequency, startTime, duration) {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(1, startTime + 0.01);
gainNode.gain.linearRampToValueAtTime(0, startTime + duration);
oscillator.frequency.value = frequency;
oscillator.type = "sine";
oscillator.start(startTime);
oscillator.stop(startTime + duration);
}
playMelody
関数を更新して、作業が完了した感じのあるメロディを再生します。この例では、C4, E4, G4, C5の音を順番に再生します。
function playMelody() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const startTime = audioContext.currentTime;
const noteDuration = 0.3;
playNote(audioContext, 261.63, startTime, noteDuration); // C4
playNote(audioContext, 329.63, startTime + noteDuration, noteDuration); // E4
playNote(audioContext, 392.00, startTime + noteDuration * 2, noteDuration); // G4
playNote(audioContext, 523.25, startTime + noteDuration * 3, noteDuration); // C5
}
archiveCurrentMessage
関数内で、`play
すごい!ちゃんとメロディになってますね。では受信完了時にメロディを鳴らすかどうかのチェックボックスをつけてください。メロディも2倍くらいの長さの、もっと工夫したものにしてください。
context_length_exceeded This model's maximum context length is 8192 tokens. However, your messages resulted in 8285 tokens. Please reduce the length of the messages.
(maximum context lengthを超えたからここらで一旦終了することにした)