Skip to content

Instantly share code, notes, and snippets.

@nishio
Last active April 4, 2023 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nishio/270a066043e6a924062b045b44ba46e7 to your computer and use it in GitHub Desktop.
Save nishio/270a066043e6a924062b045b44ba46e7 to your computer and use it in GitHub Desktop.

0: user

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行ほどのファイルを丸ごと貼り付けただけなので省略)

1: assistant

まず、部屋を選択するUIを追加するために、次のコードを追加してください。 (追記: 英語で指示したのに日本語で返事されちゃったのは「中高生のためのChatGPT」の動作テストのために自分で使ってみてて、システムプロンプトに「日本語で答えろ」という指示が入ってるせい。)

  1. ChatRoom インターフェースの下に、選択された部屋のIDを保持するためのシグナルを作成します。
const [selectedRoomId, setSelectedRoomId] = createSignal("");
  1. onMount 関数内で、localStorage から以前に選択された部屋を取得し、デフォルトの部屋を設定します。
onMount(() => {
  // ...
  const storedRoomId = localStorage.getItem("selectedRoomId") || "firstroom";
  setSelectedRoomId(storedRoomId);
  getOrCreateFirstRoom().then((roomId) => {
    setSelectedRoomId(roomId);
    restoreChatLog(roomId);
  });
  // ...
});
  1. 部屋を選択するためのドロップダウンリストを追加します。これを <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>
  1. saveChatMessagerestoreChatLog、および getOrCreateFirstRoom 関数で使用されるハードコードされた "firstroom"selectedRoomId() に置き換えます。

これで、部屋を選択するUIが追加され、選択された部屋のメッセージが更新されるようになりました。また、以前に選択された部屋が localStorage に保存され、デフォルトの部屋が "firstroom" に設定されています。

2: user

コンパイルは通った。新規ルームを作るボタンをルーム一覧の隣につけて、ルーム一覧はFirestoreから取得するようにして。

3: assistant

新規ルームを作成するボタンを追加し、Firestoreからルーム一覧を取得するようにするには、以下の手順に従ってください。

  1. 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;
}
  1. ルーム一覧を保持するためのシグナルを作成します。
const [roomList, setRoomList] = createSignal<ChatRoom[]>([]);
  1. onMount 関数内で、fetchRooms 関数を呼び出してルーム一覧を取得し、シグナルに設定します。
onMount(() => {
  // ...
  fetchRooms().then((rooms) => setRoomList(rooms));
  // ...
});
  1. 新規ルームを作成する関数を作成します。
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;
}
  1. 新規ルームを作成するボタンを追加し、クリック時に新規ルームを作成し、ルーム一覧を更新します。これを <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>
  1. <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から取得されるようになりました。新しいルームを作成すると、ルーム一覧が更新され、新しいルームが選択されます。

4: user

ボタンとルーム一覧のスタイルが他のUIと違ってダークモードにならないのだけど直して

5: assistant

ボタンとルーム一覧のスタイルを他のUIと同じようにダークモードに対応させるには、以下の手順に従ってください。

  1. 新規ルームを作成するボタンに gen-slate-btn クラスを追加します。
<button
  onClick={async () => {
    // ...
  }}
  gen-slate-btn
>
  新規ルームを作成
</button>
  1. <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>
  1. <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と同じようにダークモードに対応するようになりました。

6: user

すごいですね。バッチリです。細かい修正ですが、新規ルームを作成時にルーム名を聞く時、デフォルト値として今の日時を入れましょう。また、メッセージだけでなくSystemRoleもルームごとにFirestoreに保存すべきでした。

(追記: バッチリと書いてるが、2の指示に従うとエラーになるのでCSSに貼ってる。CSSファイルの存在はGPT4に教えてないからわからないのも仕方ない)

7: assistant

新規ルームを作成する際に、デフォルトのルーム名として現在の日時を設定し、SystemRoleもルームごとにFirestoreに保存するように変更するには、以下の手順に従ってください。

  1. 新規ルームを作成する際に、デフォルトのルーム名として現在の日時を設定します。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>
  1. 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 });
}
  1. sendMessage 関数内で、saveChatMessage 関数を呼び出す際に、currentSystemRoleSettings() を引数として渡します。
const sendMessage = async (mode: string) => {
  // ...
  saveChatMessage(selectedRoomId(), m, currentSystemRoleSettings());
  // ...
};
  1. archiveCurrentMessage 関数内で、saveChatMessage 関数を呼び出す際に、currentSystemRoleSettings() を引数として渡します。
const archiveCurrentMessage = () => {
  // ...
  saveChatMessage(selectedRoomId(), m, currentSystemRoleSettings());
  // ...
};

これで、新規ルームを作成する際にデフォルトのルーム名として現在の日時が設定され、SystemRoleもルームごとにFirestoreに保存されるようになりました。

8: user

saveChatMessageでシステムロールを保存するのではなく、システムロールの変更時に保存するようにして

9: assistant

システムロールの変更時に保存するように変更するには、以下の手順に従ってください。

  1. システムロールを保存するための新しい関数を作成します。
async function saveSystemRole(roomId: string, systemRole: string) {
  const userId = await signInAnonymously(auth);
  await db
    .collection("users")
    .doc(userId)
    .collection("rooms")
    .doc(roomId)
    .update({ systemRole });
}
  1. SystemRoleSettings コンポーネント内で、onBlur イベントを追加し、システムロールが変更されたときに saveSystemRole 関数を呼び出します。
<textarea
  // ...
  onBlur={() => saveSystemRole(selectedRoomId(), currentSystemRoleSettings())}
/>
  1. 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(),
    });
}
  1. sendMessage 関数と archiveCurrentMessage 関数から、saveChatMessage 関数へのシステムロールの引数を削除します。
const sendMessage = async (mode: string) => {
  // ...
  saveChatMessage(selectedRoomId(), m);
  // ...
};

const archiveCurrentMessage = () => {
  // ...
  saveChatMessage(selectedRoomId(), m);
  // ...
};

これで、システムロールが変更されたときにFirestoreに保存されるようになりました。メッセージの送信時にはシステムロールが保存されなくなりました。

(追記: onBlurではなく保存ボタンを押した時に保存するようにした。コンポーネントの中身のことをGPT4は知らないのでボタンで保存するのではなくフォーカスを外すだけで自動保存にしてると思ったんだね。確かにその設計の方がいいかもなー。)

10: user

SystemRoleSettings コンポーネント内で、saveSystemRole 関数を呼び出すにはpropsに渡す必要がありますね、書いて。

11: assistant

おっしゃる通りです。SystemRoleSettings コンポーネントに saveSystemRole 関数を渡すには、以下の手順に従ってください。

  1. SystemRoleSettings コンポーネントに saveSystemRole 関数を渡すための新しいプロパティを追加します。
<SystemRoleSettings
  // ...
  saveSystemRole={saveSystemRole}
/>
  1. SystemRoleSettings コンポーネントの定義で、新しいプロパティを受け取ります。
interface SystemRoleSettingsProps {
  // ...
  saveSystemRole: (roomId: string, systemRole: string) => Promise<void>;
}

const SystemRoleSettings: Component<SystemRoleSettingsProps> = (props) => {
  // ...
};
  1. SystemRoleSettings コンポーネント内で、onBlur イベントで props.saveSystemRole 関数を呼び出します。
<textarea
  // ...
  onBlur={() => props.saveSystemRole(selectedRoomId(), currentSystemRoleSettings())}
/>

これで、SystemRoleSettings コンポーネント内で saveSystemRole 関数が呼び出されるようになりました。システムロールが変更されたときにFirestoreに保存されます。

12: user

ルームが変更された時にシステムロールをFirestoreから読むコードも必要ですね

13: assistant

ルームが変更されたときにシステムロールをFirestoreから読み込むには、以下の手順に従ってください。

  1. システムロールを取得するための新しい関数を作成します。
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;
}
  1. <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から読み込まれ、現在のシステムロールに設定されるようになりました。

14: user

良さそうです。さらに機能追加です。smoothToBottomを呼び出すかどうかを指定するチェックボックスをつけてください。また受信が完了した時にメロディを鳴らしてお知らせするようにしてください。

15: assistant

機能を追加するには、以下の手順に従ってください。

  1. smoothToBottomを呼び出すかどうかを指定するためのシグナルを作成します。
const [enableSmoothToBottom, setEnableSmoothToBottom] = createSignal(true);
  1. smoothToBottomを呼び出すかどうかを指定するチェックボックスを追加します。これを <div> 要素の後に追加してください。
<label>
  <input
    type="checkbox"
    checked={enableSmoothToBottom()}
    onChange={(e) => setEnableSmoothToBottom(e.currentTarget.checked)}
  />
  スクロールを自動で下へ
</label>
  1. requestWithLatestMessage 関数内で、enableSmoothToBottom シグナルの値に基づいて smoothToBottom を呼び出すかどうかを判断します。
if (enableSmoothToBottom()) {
  smoothToBottom();
}
  1. 受信が完了したときにメロディを鳴らすために、archiveCurrentMessage 関数内で Audio オブジェクトを作成し、音声ファイルを再生します。音声ファイルはプロジェクト内に配置してください。
const archiveCurrentMessage = () => {
  // ...
  const audio = new Audio("/path/to/your/audio-file.mp3");
  audio.play();
};

これで、smoothToBottomを呼び出すかどうかを指定するチェックボックスが追加され、受信が完了したときにメロディが鳴るようになりました。

16: user

audio自体を作って。

17: assistant

オーディオを作成するには、Web Audio APIを使用して、次の手順に従ってください。

  1. オーディオコンテキストを作成し、オシレーターとゲインノードを設定します。
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);
}
  1. archiveCurrentMessage 関数内で、playMelody 関数を呼び出します。
const archiveCurrentMessage = () => {
  // ...
  playMelody();
};

これで、受信が完了したときにメロディが鳴るようになりました。このコードは、440Hzのサイン波を1秒間再生します。周波数や再生時間を変更することで、異なるメロディを作成できます。

18: user

もうちょっと工夫して作業が完了した感じのあるメロディを作曲して

19: assistant

作業が完了した感じのあるメロディを作成するには、以下の手順に従ってください。

  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);
}
  1. 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
}
  1. archiveCurrentMessage 関数内で、`play

20: user

すごい!ちゃんとメロディになってますね。では受信完了時にメロディを鳴らすかどうかのチェックボックスをつけてください。メロディも2倍くらいの長さの、もっと工夫したものにしてください。

error

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を超えたからここらで一旦終了することにした)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment