ChatGPT-4に、Google Apps Script + ReactでChatGPT APIで会話できるウェブアプリを作ってもらいました。
機能追加したものはこちらです。ChatGPTのウェブ画面のように、会話一覧を左側に掲載しています。
シンプル版
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<script src="https://unpkg.com/react-markdown@4.3.1/umd/react-markdown.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class ResizableTextarea extends React.Component {
constructor(props) {
super(props);
this.textareaRef = React.createRef();
}
handleChange = (event) => {
this.props.onChange(event);
this.handleInput(event);
};
handleInput = (event) => {
const textarea = this.textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
render() {
const { name, value, onKeyPress } = this.props;
return (
<textarea
ref={this.textareaRef}
name={name}
value={value}
onChange={this.handleChange}
onKeyPress={onKeyPress}
rows="3"
style={{ width: "100%" }}
/>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
maxTokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
chatAreaMaxHeight: this.calculateChatAreaMaxHeight(),
maxConversationLength: 10,
};
}
calculateChatAreaMaxHeight = () => {
return window.innerHeight * 0.65;
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.setState({ chatAreaMaxHeight: this.calculateChatAreaMaxHeight() });
};
componentDidUpdate() {
if (this.chatArea) {
this.chatArea.scrollTop = this.chatArea.scrollHeight;
}
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = async (event) => {
event.preventDefault();
const newMessage = { role: "user", content: this.state.input };
this.setState((prevState) => ({
messages: [...prevState.messages, newMessage],
input: "",
loading: true,
}));
const { input, system, temperature, maxTokens, messages, maxConversationLength } = this.state;
const conversation = [...messages, newMessage].slice(-maxConversationLength);
google.script.run
.withSuccessHandler((response) => {
this.setState((prevState) => ({
messages: [...prevState.messages, { role: "assistant", content: response }],
loading: false,
}));
})
.sendMessageToChatGPT(conversation, system, temperature, maxTokens);
};
toggleAdvancedSettings = () => {
this.setState((prevState) => ({
showAdvancedSettings: !prevState.showAdvancedSettings,
}));
};
render() {
const { input, system, temperature, maxTokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div
ref={(el) => { this.chatArea = el; }}
style={{
maxHeight: `${this.state.chatAreaMaxHeight}px`,
overflowY: 'scroll',
marginBottom: '1em',
}}
>
{messages.map((message, index) => (
<p key={index} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.role === "user" ? "User" : "ChatGPT"}:</strong>
<br />
{message.role === "user" ? (
message.content
) : (
<ReactMarkdown>{message.content}</ReactMarkdown>
)}
</p>
))}
{loading && (
<div className="spinner" />
)}
</div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="maxTokens">Max tokens:</label>
<input
type="number"
name="maxTokens"
value={maxTokens}
min="1"
onChange={this.handleChange}
/>
<label htmlFor="maxConversationLength">Max conversation length:</label>
<input
type="number"
name="maxConversationLength"
value={maxConversationLength}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
スクリプトプロパティOPENAI_API_KEY
にAPI Keyを保存しておきます。
const ENV = 'prod';
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index.html');
}
// https://platform.openai.com/docs/api-reference/chat/create
function chatCompletion(args) {
if (!args.messages) throw 'chatCompletion: messages is required.';
const openApiKey = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
if (!openApiKey) throw 'chatCompletion: Script Property OPENAI_API_KEY is required.';
args = Object.assign({
model: 'gpt-3.5-turbo',
}, args);
const url = 'https://api.openai.com/v1/chat/completions';
const res = UrlFetchApp.fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${openApiKey}`,
},
payload: JSON.stringify(args),
muteHttpExceptions: true
});
return JSON.parse(res.getContentText());
}
function sendMessageToChatGPT(conversation, system, temperature, max_tokens) {
if (ENV !== 'prod') {
const input = conversation.slice(-1)[0];
return `Response for "${input.content}"`;
}
const args = {
messages: system ? [{ role: 'system', content: system }, ...conversation] : conversation,
}
if (temperature) args.temperature = temperature;
if (max_tokens) args.max_tokens = max_tokens;
console.log('Request:', args);
const json = chatCompletion(args);
console.log('Response:', JSON.stringify(json, null, 2));
return json.choices[0].message.content;
}
以下に、ChatGPT-4 とのやり取りをそのまま記載します。以下のブックマークレットで抽出しました。
React ChatGPT アプリ
あなた:
ReactでChatGPTとチャットできるアプリを作ってください。ただし、Google Apps Scriptのウェブアプリとして作成します。フロントエンドはgoogle.script.run()を使って通信します。セットアップの方法は不要で、ソースコードだけ提示してください。
3 / 3
ChatGPT:
以下は、React を使用して Google Apps Script のウェブアプリで ChatGPT とチャットできるアプリのソースコードです。
まず、Google Apps Script 側のコードを準備します。
// Google Apps Script
function doGet() {
return HtmlService.createHtmlOutputFromFile('index.html')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function sendMessageToChatGPT(message) {
// ここに OpenAI API を使って ChatGPT とのやりとりを実装してください。
// 必要な認証情報やリクエストの形式に注意してください。
const response = 'ChatGPT の応答: ' + message;
return response;
}
次に、index.html ファイルとして、React アプリケーションを作成します。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
messages: [],
};
}
handleChange = (event) => {
this.setState({ input: event.target.value });
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
});
})
.sendMessageToChatGPT(this.state.input);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
</div>
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
このソースコードは、React を使用して ChatGPT とチャットできるアプリを Google Apps Script のウェブアプリとして作成します。ただし、sendMessageToChatGPT 関数の実装は簡素化されているため、OpenAI API を使用して実際に ChatGPT と通信するコードを追加する必要があります。
あなた:
いい感じです。どうもありがとう。Shift + Enterを押したときは送信せずに、続きをかけるようにできますか?
ChatGPT:
もちろんできます!以下のように index.html の <script>
タグ内の App コンポーネントのイベントハンドラを変更して、Shift + Enter キーが押されたときに改行を挿入し、Enter キーのみが押されたときに送信するようにします。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
messages: [],
};
}
handleChange = (event) => {
this.setState({ input: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
});
})
.sendMessageToChatGPT(this.state.input);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
</div>
<form onSubmit={this.handleSubmit}>
<textarea
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
handleKeyPress メソッドを追加し、onKeyPress イベントでこれを使用するようにしました。これにより、Shift + Enter キーが押されたときに改行が挿入され、Enter キーのみが押されたときに送信されます。また、 要素を 要素に変更して、改行がサポートされるようにしました。
あなた:
ChatGPT APIを使うときに、systemパラメーターも必要になります。systemパラメーターを入力できる欄をフロントエンドに追加してください。
ChatGPT:
system パラメータを入力できる欄を追加するには、以下のように index.html の <script>
タグ内の App コンポーネントを変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
messages: [],
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
});
})
.sendMessageToChatGPT(this.state.input, this.state.system);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
</div>
<form onSubmit={this.handleSubmit}>
<label>
System parameter:
<input
type="text"
name="system"
value={this.state.system}
onChange={this.handleChange}
/>
</label>
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
以下の変更が加えられました。
- state に system プロパティを追加しました。
- handleChange メソッドで、event.target.name を使って入力フィールドの name 属性に基づいて state を更新できるようにしました。
- system パラメータ用の入力フィールドを追加し、name 属性を設定しました。これにより、handleChange メソッドで state.system が更新されます。
- handleSubmit メソッドで、system パ
あなた:
どうもありがとう。返信が来るまでの間、スロバーを表示してください。
ChatGPT:
返信が来るまでの間にスロバー(ローディングインジケータ)を表示するには、以下のように index.html の <script>
タグ内の App コンポーネントを変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
messages: [],
loading: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
loading: true,
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
{this.state.loading && <div className="spinner"></div>}
</div>
<form onSubmit={this.handleSubmit}>
<label>
System parameter:
<input
type="text"
name="system"
value={this.state.system}
onChange={this.handleChange}
/>
</label>
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
変更点は以下の通りです。
-
<style>
タグを追加し、スピナーの
あなた:
スロバーを表示しているときは、Sendボタンを無効にしてください。
ChatGPT:
返信が来るまでの間にスロバー(ローディングインジケータ)を表示しているときに、Send ボタンを無効にするには、以下のように index.html の <script>
タグ内の App コンポーネントを変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
messages: [],
loading: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
loading: true,
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
{this.state.loading && <div className="spinner"></div>}
</div>
<form onSubmit={this.handleSubmit}>
<label>
System parameter:
<input
type="text"
name="system"
value={this.state.system}
onChange={this.handleChange}
/>
</label>
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit" disabled={this.state.loading}>Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
<button>
タグ
あなた:
とてもいいですね!テキストエリアの位置を調整しましょう。system parameterのサイズが小さくて、チャット入力欄が大きくなっています。system parameterのtextareaのサイズをチャット入力欄と同じ大きさに大きくしてください。
ChatGPT:
system parameterのサイズをチャット入力欄と同じ大きさにするには、以下のように index.html の <style>
タグ内にCSSを追加し、<input>
タグを <textarea>
タグに変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
messages: [],
loading: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
loading: true,
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system);
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
{this.state.loading && <div className="spinner"></div>}
</div>
<form onSubmit={this.handleSubmit}>
<label>
System parameter:
<textarea
name="system"
value={this.state.system}
onChange={this.handleChange}
rows="3"
style={{ width: "100%" }}
/>
</label>
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit" disabled={this.state.loading}>Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
</body>
</html>
変
あなた:
すごくいいですね!ただ、System parameterの欄が常に表示されているのが気になります。Advancedをクリックすると、System parameterの入力欄が表示され、表示されている状態でAdvancedをクリックすると、今度はSystem parameterの入力欄が隠れるようにすることはできますか?
ChatGPT:
もちろんできます!Advanced クリック時に System parameter の入力欄を表示・非表示するように、以下のように index.html の <script>
タグ内の App コンポーネントを変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
messages: [],
loading: false,
showSystemInput: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
loading: true,
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system);
};
toggleSystemInput = () => {
this.setState({ showSystemInput: !this.state.showSystemInput });
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
{this.state.loading && <div className="spinner"></div>}
</div>
<form onSubmit={this.handleSubmit}>
<button type="button" onClick={this.toggleSystemInput}>Advanced</button>
{this.state.showSystemInput && (
<label>
System parameter:
<textarea
name="system"
value={this.state.system}
onChange={this.handleChange}
rows="3"
style={{ width: "100%" }}
/>
</label>
)}
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
あなた:
続けて
ChatGPT:
<button type="submit" disabled={this.state.loading}>Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
これで、Advanced ボタンをクリックすると System parameter の入力欄が表示され、もう一度クリックすると非表示になるようになります。
あなた:
素晴らしいです!Advancedな設定として、System parameterに加えて、temperatureとmax_tokensの2つを加えます。temperatureは0から2までの値を取れる小数です。max_tokensは整数です。これら2つについて設定できるようにしてください。ただし、Advancedボタンを押すと、表示されるようにします。
ChatGPT:
Advanced 設定に temperature と max_tokens を追加するには、以下のように index.html の <script>
タグ内の App コンポーネントを変更してください。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({
messages: [...this.state.messages, { sender: "User", content: this.state.input }],
input: "",
loading: true,
});
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system, this.state.temperature, this.state.max_tokens);
};
toggleAdvancedSettings = () => {
this.setState({ showAdvancedSettings: !this.state.showAdvancedSettings });
};
render() {
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{this.state.messages.map((message, index) => (
<p key={index}><strong>{message.sender}:</strong> {message.content}</p>
))}
{this.state.loading && <div className="spinner"></div>}
</div>
<form onSubmit={this.handleSubmit}>
<button type="button" onClick={this.toggleAdvancedSettings}>Advanced</button>
{this.state.showAdvancedSettings && (
<div>
<label>
System parameter:
<textarea
name="system"
value={this.state.system}
onChange={this.handleChange}
rows="3"
style={{ width: "100%" }}
/>
</label>
<br />
<label>
Temperature:
<input
type="number"
name="temperature"
value={this.state.temperature}
onChange={this.handleChange}
min="0"
あなた:
コードの出力を続けて
ChatGPT:
max="2"
step="0.01"
/>
</label>
<br />
<label>
Max tokens:
<input
type="number"
name="max_tokens"
value={this.state.max_tokens}
onChange={this.handleChange}
min="1"
step="1"
/>
</label>
</div>
)}
<br />
<textarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
<button type="submit" disabled={this.state.loading}>Send</button>
</form>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
これで、Advanced ボタンをクリックすると temperature と max_tokens の入力欄が表示されます。また、入力された値は sendMessageToChatGPT 関数に引数として渡されます。Code.gs の sendMessageToChatGPT 関数も、新しい引数を受け取るように修正する必要があります。
あなた:
素晴らしいですね!どうもありがとうございます。今度は、textareaについての質問です。Shift + Enterでたくさんの文字を入力できるのはよいのですが、入力の行が増えるのに応じて、textareaの高さを伸ばすことはできますか?
ChatGPT:
もちろんできます!textarea の高さを入力行に合わせて自動的に伸ばすには、以下のように index.html の <script>
タグ内の App コンポーネントに handleInput 関数を追加してください。そして、この関数を onChange イベントと onKeyPress イベントで呼び出すように変更します。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
this.textareaRef = React.createRef();
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
this.handleInput(event);
};
handleInput = (event) => {
this.handleChange(event);
const textarea = this.textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
// handleSubmit, toggleAdvancedSettings, and render functions remain the same
}
// Render function remains the same
</script>
</body>
</html>
このコードでは、handleInput 関数が入力が変更されるたびに textarea の高さを自動的に調整します。また、React.createRef() を使用して、textarea への参照を作成しています。最後に、textarea 要素に ref プロパティを追加して、作成した参照を割り当てます。
<textarea
ref={this.textareaRef}
name="input"
value={this.state.input}
onChange={this.handleInput}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width: "100%" }}
/>
これで、入力行が増えるに応じて textarea の高さが伸びるようになります。
あなた:
素晴らしい!同じ仕組みをsystem parameterのtextareaにも適用したいです。
ChatGPT:
もちろん、同じ仕組みを System parameter の textarea に適用することができます。index.html の <script>
タグ内の App コンポーネントに、System parameter 用の handleSystemParameterInput 関数を追加し、それを onChange イベントで呼び出すように変更します。
また、新しい React.createRef() を使用して、System parameter の textarea への参照を作成します。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
this.textareaRef = React.createRef();
this.systemTextareaRef = React.createRef();
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
this.handleInput(event);
};
handleInput = (event) => {
this.handleChange(event);
const textarea = this.textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
handleSystemParameterInput = (event) => {
this.handleChange(event);
const textarea = this.systemTextareaRef.current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
// handleSubmit, toggleAdvancedSettings, and render functions remain the same
}
// Render function remains the same
</script>
</body>
</html>
最後に、System parameter の textarea 要素に ref プロパティを追加して、作成した参照を割り当てます。また、onChange イベントで handleSystemParameterInput 関数が呼び出されるようにします。
<textarea
ref={this.systemTextareaRef}
name="system"
value={this.state.system}
onChange={this.handleSystemParameterInput}
rows="3"
style={{ width: "100%" }}
/>
これで
あなた:
ありがとうございます。ただ、似たようなコードをわざわざ書きたくないので、うまく共通化できないでしょうか?
ChatGPT:
もちろんです!handleInput 関数を汎用化して、異なる textarea にも対応できるように変更しましょう。handleInput 関数がどの textarea に対して動作するかを引数として受け取るようにします。
以下のように index.html の <script>
タグ内の App コンポーネントを変更します。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
this.textareaRefs = {
input: React.createRef(),
system: React.createRef(),
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
this.handleInput("input", event);
};
handleInput = (name, event) => {
this.handleChange(event);
const textarea = this.textareaRefs[name].current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
// handleSubmit, toggleAdvancedSettings, and render functions remain the same
}
// Render function remains the same
</script>
</body>
</html>
handleInput 関数は、引数 name を受け取り、どの textarea に対して動作するかを決定します。また、this.textareaRefs を使用して、すべての textarea の参照を保持します。
最後に、各 textarea 要素の onChange イベントで handleInput 関数を呼び出し、どの textarea に対して動作するかを指定します。
<textarea
ref={this.textareaRefs.input}
name="input"
value={this.state.input}
onChange={(event) => this.handleInput("input", event)}
onKeyPress={this.handleKeyPress}
rows="3"
style={{ width
あなた:
いいですね!さらに、textareaをコンポーネントとして共通化することはできますか?
ChatGPT:
もちろんです!ResizableTextarea という新しいコンポーネントを作成し、handleChange と handleInput 関数を共通化して使用できるようにしましょう。以下のように index.html の <script>
タグ内に新しいコンポーネントを追加します。
<!DOCTYPE html>
<html>
<head>
<title>ChatGPT with React</title>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script>
<style>
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #3f51b5;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
textarea {
resize: none;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class ResizableTextarea extends React.Component {
constructor(props) {
super(props);
this.textareaRef = React.createRef();
}
handleChange = (event) => {
this.props.onChange(event);
this.handleInput(event);
};
handleInput = (event) => {
const textarea = this.textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
};
render() {
const { name, value, onKeyPress } = this.props;
return (
<textarea
ref={this.textareaRef}
name={name}
value={value}
onChange={this.handleChange}
onKeyPress={onKeyPress}
rows="3"
style={{ width: "100%" }}
/>
);
}
}
class App extends React.Component {
// App component implementation remains the same
}
// Render function remains the same
</script>
</body>
</html>
ResizableTextarea コンポーネントは、handleChange と handleInput 関数を内部で定義し、onChange イベントで共通の処理を行います。このコンポーネントは、name、value、および onKeyPress のプロパティを受け取り、各 textarea に適用します。
最後に、App コンポーネントの render 関数内で、ResizableTextarea コンポーネントを使用して textarea を置き換えます。
<ResizableTextarea
name="input"
value={this.state.input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<ResizableTextarea
name="system"
value={this.state.system}
onChange={this.handleChange}
/>
これで、textarea の共通化が完了しました。
あなた:
素晴らしい!この結果として、class App のソースコードはどうなりましたか? class App のソースコードだけ示してください。
ChatGPT:
ResizableTextarea コンポーネントの導入により、class App のソースコードが以下のようになります。handleInput 関数は削除され、handleChange と handleKeyPress 関数が簡略化されました。また、render 関数で ResizableTextarea コンポーネントが使用されています。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
// API call implementation remains the same
};
toggleAdvancedSettings = () => {
this.setState((prevState) => ({
showAdvancedSettings: !prevState.showAdvancedSettings,
}));
};
render() {
const { input, system, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
{/* Render implementation remains the same, except for the usage of ResizableTextarea components */}
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
{showAdvancedSettings && (
<>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
{/* Other advanced settings and form elements remain the same */}
</>
)}
</div>
);
}
}
これで、class App のソースコードが共通化され、よりシンプルになりました。
あなた:
class App の render() のコードを見せて
ChatGPT:
class App の render() 関数のコードは以下のようになります。ResizableTextarea コンポーネントが使用されている点に注意してください。
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index}>{message}</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</>
)}
</form>
</div>
);
}
この render() 関数により、アプリケーションの UI が構築されます。ResizableTextarea コンポーネントを使用して、メッセージ入力欄とシステムパラメータ入力欄が表示されます。また、Advanced ボタンをクリックすることで、その他の詳細設定が表示されます。
あなた:
render()の中で使っている<>
は使えず、<div>
なら大丈夫でした。
ChatGPT:
すみません。<>(フラグメント)が使用できない場合は、`
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index}>{message}</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
この修正により、Advanced 設定が表示される際に、<div>
タグを使用して要素がラップされます。このようにして、<> を使わずに要素をラップできます。
あなた:
class App の handleSubmit() のコードを見せてください。ただし、Google Apps Scriptを使っているので、サーバーの呼び出しは次のコードになります。
google.script.run
.withSuccessHandler((response) => {
this.setState({
messages: [...this.state.messages, { sender: "ChatGPT", content: response }],
loading: false,
});
})
.sendMessageToChatGPT(this.state.input, this.state.system, this.state.temperature, this.state.max_tokens);
2 / 2
ChatGPT:
ご指定いただいたサーバー呼び出しコードを使用して、class App の handleSubmit() 関数を次のように修正しました。
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
const { input, system, temperature, max_tokens } = this.state;
google.script.run
.withSuccessHandler((response) => {
this.setState((prevState) => ({
messages: [...prevState.messages, { sender: "User", content: input }, { sender: "ChatGPT", content: response }],
input: "",
loading: false,
}));
})
.sendMessageToChatGPT(input, system, temperature, max_tokens);
};
この修正版の handleSubmit() 関数では、Google Apps Script を使ってサーバーにメッセージを送信し、レスポンスを受け取るようになっています。withSuccessHandler() を使って成功時の処理を定義しており、チャットのメッセージリストにユーザーと ChatGPT のメッセージが追加されます。
あなた:
class App を次のように書いたのですが、withSuccessHandlerのところでエラーが出て、画面が表示されません。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
system: "",
temperature: "",
max_tokens: "",
messages: [],
loading: false,
showAdvancedSettings: false,
};
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
handleKeyPress = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
this.handleSubmit(event);
}
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
const { input, system, temperature, max_tokens } = this.state;
google.script.run
.withSuccessHandler((response) => {
this.setState((prevState) => ({
messages: [...prevState.messages, { sender: "User", content: input }, { sender: "ChatGPT", content: response }],
input: "",
loading: false,
}));
})
.sendMessageToChatGPT(input, system, temperature, max_tokens);
};
toggleAdvancedSettings = () => {
this.setState((prevState) => ({
showAdvancedSettings: !prevState.showAdvancedSettings,
}));
};
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index}>{message}</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
}
2 / 2
ChatGPT:
ご提示いただいたコードを確認しました。messages.map() 関数内で、message がオブジェクトであるためにエラーが発生している可能性があります。<p key={index}>{message}</p>
の部分を <p key={index}>{message.content}</p>
に修正して、オブジェクトの content プロパティを表示するようにしてみてください。
以下に修正済みの render() 関数を示します。
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index}>{message.content}</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
この修正により、messages 配列内のオブジェクトの content プロパティが正しく表示されるはずです。画面が表示されるかどうか確認してください。もしまだ問題が解決しない場合は、エラーメッセージをお知らせください。
あなた:
うまくいきました!どうもありがとう!
私がtextareaに入力したテキストが、改行なしで画面に表示されます。改行ありで画面に表示できますか?
ChatGPT:
テキストエリアからのテキストを改行込みで表示するには、改行をHTMLの
タグに置き換えて表示する必要があります。以下のように、render()関数内のメッセージ表示部分を修正して、CSSプロパティwhite-space: pre-wrap;を適用することで、改行を表示できます。
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index} style={{ whiteSpace: 'pre-wrap' }}>{message.content}</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
これで、メッセージが改行込みで表示されるようになります。
あなた:
どうもありがとうございます!動きました。
私が入力したものはYou:のあとに改行して、ChatGPTが返信したものはChatGPT:の後に改行して、それぞれ表示するようにコードを変えられますか?
ChatGPT:
入力されたメッセージとChatGPTの返信を、それぞれ "You:" および "ChatGPT:" の後に改行して表示するには、次のように render() 関数内のメッセージ表示部分を修正してください。
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.sender}:</strong>
<br />
{message.content}
</p>
))}
</div>
{loading && (
<div className="spinner" />
)}
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
これで、入力されたメッセージとChatGPTの返信が、それぞれ "You:" および "ChatGPT:" の後に改行されて表示されるようになります。
あなた:
いいですね!
ただ、Your messageに入力した内容はUser:として表示してスロバーを見せて、ChatGPTからの返信が届いたら、ChatGPT:として表示したいです。
ChatGPT:
要求に従って、ユーザーがメッセージを送信したときに "User:" として表示し、スロバーを表示した後に、ChatGPTからの返信が届いたら "ChatGPT:" として表示するには、handleSubmit() 関数と render() 関数を次のように変更してください。
- handleSubmit()関数で、メッセージを送信する前に "User:" のメッセージを messages ステートに追加します。
- render() 関数で、ローディング中のスロバーを表示します。
handleSubmit = async (event) => {
event.preventDefault();
this.setState((prevState) => ({
messages: [...prevState.messages, { sender: "User", content: prevState.input }],
input: "",
loading: true,
}));
const { input, system, temperature, max_tokens } = this.state;
google.script.run
.withSuccessHandler((response) => {
this.setState((prevState) => ({
messages: [...prevState.messages, { sender: "ChatGPT", content: response }],
loading: false,
}));
})
.sendMessageToChatGPT(input, system, temperature, max_tokens);
};
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div>
{messages.map((message, index) => (
<p key={index} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.sender}:</strong>
<br />
{message.content}
</p>
))}
{loading && (
<div className="spinner" />
)}
</div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="input">Your message:</label>
<ResizableTextarea
name="input"
value={input}
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
/>
<button type="submit" disabled={loading}>Send</button>
<button type="button" onClick={this.toggleAdvancedSettings}>
Advanced
</button>
{showAdvancedSettings && (
<div>
<label htmlFor="system">System parameter:</label>
<ResizableTextarea
name="system"
value={system}
onChange={this.handleChange}
/>
<label htmlFor="temperature">Temperature:</label>
<input
type="number"
name="temperature"
value={temperature}
min="0"
max="2"
step="0.01"
onChange={this.handleChange}
/>
<label htmlFor="max_tokens">Max tokens:</label>
<input
type="number"
name="max_tokens"
value={max_tokens}
min="1"
onChange={this.handleChange}
/>
</div>
)}
</form>
</div>
);
}
これで、ユーザーがメッセージを送信すると、送信されたメッセージが "User:" として表示され、スロバーが表示され、その後に ChatGPT からの返信が "ChatGPT:" として表示されるようになります。
あなた:
素晴らしいですね!maxHeightをウィンドウの高さに合わせて動的に変更することはできますか?
2 / 2
ChatGPT:
もちろんできます!ウィンドウの高さに応じて、チャットエリアのmaxHeightを動的に変更するには、window.innerHeightを使用してウィンドウの高さを取得し、適切な値を計算してスタイルに適用します。また、ウィンドウがリサイズされたときにも対応できるように、resizeイベントリスナーを追加します。
以下の手順でコードを変更してください。
- ウィンドウの高さに応じてチャットエリアのmaxHeightを動的に設定するための関数を追加します。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
// ...
chatAreaMaxHeight: this.calculateChatAreaMaxHeight(),
};
}
calculateChatAreaMaxHeight = () => {
return window.innerHeight * 0.5;
}
// ...
}
この例では、ウィンドウの高さの50%をmaxHeightとして設定していますが、適切な値に変更してください。
- ウィンドウがリサイズされたときにmaxHeightを更新するために、resizeイベントリスナーを追加および削除します。
class App extends React.Component {
// ...
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.setState({ chatAreaMaxHeight: this.calculateChatAreaMaxHeight() });
};
// ...
}
- render()関数内で、maxHeightを動的に設定します。
// ...
render() {
const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings, chatAreaMaxHeight } = this.state;
return (
<div>
<h1>ChatGPT with React</h1>
<div
ref={(el) => { this.chatArea = el; }}
style={{
maxHeight: `${chatAreaMaxHeight}px`,
overflowY: 'scroll',
marginBottom: '1em',
}}
>
{messages.map((message, index) => (
<p key={index} style={{ whiteSpace: 'pre-wrap' }}>
<strong>{message.sender}:</strong>
<br />
{message.content}
</p>
))}
{loading && (
<div className="spinner" />
)}
</div>
<form onSubmit={this.handleSubmit}>
//...
これで、ウィンドウの高さに合わせてチャットエリアのmaxHeightが動的に変更されるようになりました。また、ウィンドウがリサイズされたときにも、適切にmaxHeightが更新されます。
あなた:
maxHeight: ${chatAreaMaxHeight}px
,
は
maxHeight: ${this.chatAreaMaxHeight}px
,
にしないと動きませんでした。さらに、会話が長くなっても、Your messageが下に消えてしまいますね。
ChatGPT:
すみません、maxHeightの設定でミスがありました。また、Your messageが画面下部に固定されるように、次の手順でコードを変更しましょう。
- render()メソッドの最後で、をで囲み、固定された位置に表示されるようにスタイルを設定します。
render() { // ... return ( <div> <h1>ChatGPT with React</h1> <div ref={(el) => { this.chatArea = el; }} style={{ maxHeight: `${this.state.chatAreaMaxHeight}px`, overflowY: 'scroll', marginBottom: '1em', paddingBottom: '1em', }} > {messages.map((message, index) => ( // ... ))} {loading && ( <div className="spinner" /> )} </div> <div style={{ position: "fixed", bottom: 0, width: "100%" }}> <form onSubmit={this.handleSubmit}> // ... </form> </div> </div> ); }
これで、Your messageの入力欄が画面下部に固定されるようになりました。ウィンドウがリサイズされたときにも、Your message入力欄は下部に固定され続けます。
あなた:
maxHeight:
${chatAreaMaxHeight}px
,
を、
maxHeight:${this.state.chatAreaMaxHeight}px
,
にするだけで大丈夫でした!なので、formをdivで囲う必要はありませんでした。ChatGPT:
すみません、それは私のミスでした。ご指摘いただいた通り、次のように修正すれば問題なく動作します。
render() { // ... return ( <div> <h1>ChatGPT with React</h1> <div ref={(el) => { this.chatArea = el; }} style={{ maxHeight: `${this.state.chatAreaMaxHeight}px`, overflowY: 'scroll', marginBottom: '1em', paddingBottom: '1em', }} > {messages.map((message, index) => ( // ... ))} {loading && ( <div className="spinner" /> )} </div> <form onSubmit={this.handleSubmit}> // ... </form> </div> ); }
これで、ウィンドウの高さに合わせてmaxHeightが動的に変更され、Your messageの入力欄が画面下部に表示されます。
あなた:
コードを修正して、sendMessageToChatGPTを使って会話の履歴を送信します。会話の履歴は配列で、Userの発言は role:"user"、ChatGPTの発言は role:"assistant" として送信します。
ChatGPT:
以下のコードでは、sendMessageToChatGPT関数を変更し、会話履歴を配列として渡すようになっています。また、handleSubmitメソッドを変更して、新しいsendMessageToChatGPT関数の形式に適合するようにしています。
<!DOCTYPE html> <html> <head> <title>ChatGPT with React</title> <script src="https://unpkg.com/react@17/umd/react.production.min.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/babel-standalone@6.26.0/babel.min.js"></script> <style> .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-left-color: #3f51b5; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } textarea { resize: none; } </style> </head> <body> <div id="root"></div> <script type="text/babel"> class ResizableTextarea extends React.Component { constructor(props) { super(props); this.textareaRef = React.createRef(); } handleChange = (event) => { this.props.onChange(event); this.handleInput(event); }; handleInput = (event) => { const textarea = this.textareaRef.current; textarea.style.height = "auto"; textarea.style.height = textarea.scrollHeight + "px"; }; render() { const { name, value, onKeyPress } = this.props; return ( <textarea ref={this.textareaRef} name={name} value={value} onChange={this.handleChange} onKeyPress={onKeyPress} rows="3" style={{ width: "100%" }} /> ); } } class App extends React.Component { constructor(props) { super(props); this.state = { input: "", system: "", temperature: "", max_tokens: "", messages: [], loading: false, showAdvancedSettings: false, chatAreaMaxHeight: this.calculateChatAreaMaxHeight(), }; } calculateChatAreaMaxHeight = () => { return window.innerHeight * 0.65; } componentDidMount() { window.addEventListener('resize', this.handleResize); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); } handleResize = () => { this.setState({ chatAreaMaxHeight: this.calculateChatAreaMaxHeight() }); }; componentDidUpdate() { if (this.chatArea) { this.chatArea.scrollTop = this.chatArea.scrollHeight; } } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleKeyPress = (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); this.handleSubmit(event); } }; handleSubmit = async (event) => { event.preventDefault(); const newMessage = { role: "user", content: this.state.input }; this.setState((prevState) => ({ messages: [...
あなた:
続けて。ただし、```javascrpt、ではじめて。
ChatGPT:
続けます。以下のコードは、先ほどの続きで、handleSubmitメソッドを変更し、新しいsendMessageToChatGPT関数の形式に適合するようにしています。コードはJavaScriptで書かれています。
this.state.messages, newMessage], input: "", loading: true, })); const { input, system, temperature, max_tokens, messages } = this.state; const conversation = [...messages, newMessage]; google.script.run .withSuccessHandler((response) => { this.setState((prevState) => ({ messages: [...prevState.messages, { role: "assistant", content: response }], loading: false, })); }) .sendMessageToChatGPT(conversation, system, temperature, max_tokens); }; toggleAdvancedSettings = () => { this.setState((prevState) => ({ showAdvancedSettings: !prevState.showAdvancedSettings, })); }; render() { const { input, system, temperature, max_tokens, messages, loading, showAdvancedSettings } = this.state; return ( <div> <h1>ChatGPT with React</h1> <div ref={(el) => { this.chatArea = el; }} style={{ maxHeight: `${this.state.chatAreaMaxHeight}px`, overflowY: 'scroll', marginBottom: '1em', }} > {messages.map((message, index) => ( <p key={index} style={{ whiteSpace: 'pre-wrap' }}> <strong>{message.role === "user" ? "User" : "ChatGPT"}:</strong> <br /> {message.content} </p> ))} {loading && ( <div className="spinner" /> )} </div> <form onSubmit={this.handleSubmit}> <label htmlFor="input">Your message:</label> <ResizableTextarea name="input" value={input} onChange={this.handleChange} onKeyPress={this.handleKeyPress} /> <button type="submit" disabled={loading}>Send</button> <button type="button" onClick={this.toggleAdvancedSettings}> Advanced </button> {showAdvancedSettings && ( <div> <label htmlFor="system">System parameter:</label> <ResizableTextarea name="system" value={system} onChange={this.handleChange} /> <label htmlFor="temperature">Temperature:</label> <input type="number" name="temperature" value={temperature} min="0" max="1" step="0.01" onChange={this.handleChange} /> <label htmlFor="max_tokens">Max tokens:</label> <input type="number" name="max_tokens" value={max_tokens} min="1" onChange={this.handleChange} /> </div> )} </form> </div> ); } } ReactDOM.render(<App />, document.getElementById("root")); </script> </body> </html>
このコードでは、handleSubmitメソッド内で新しいメッセージをnewMessageオブジェクトに格納し、messages配列に追加します。その後、sendMessageToChatGPT関数に会話履歴としてconversation配列を渡しています。
あなた:
素晴らしいです!さらに、Advancedで、送信するconversationの上限数を設定できるようにしてください。変更するコードだけ示してください。
2 / 2ChatGPT:
Advanced設定に会話の上限数を追加するには、まずstateに新しいプロパティmax_conversation_lengthを追加し、それをフォームの入力フィールドで操作できるようにします。その後、handleSubmitメソッドを変更して、指定された上限数に基づいて会話履歴を切り詰めます。
以下のコードスニペットでは、変更が必要な部分だけを示しています。
- stateにmax_conversation_lengthプロパティを追加して初期値を設定します。
constructor(props) { super(props); this.state = { input: "", system: "", temperature: "", max_tokens: "", messages: [], loading: false, showAdvancedSettings: false, chatAreaMaxHeight: this.calculateChatAreaMaxHeight(), max_conversation_length: 10, // Add this line }; }
- Advanced設定に、max_conversation_lengthを変更できる入力フィールドを追加します。
{showAdvancedSettings && ( <div> {/* ... other fields */} <label htmlFor="max_conversation_length">Max conversation length:</label> <input type="number" name="max_conversation_length" value={max_conversation_length} min="1" onChange={this.handleChange} /> </div> )}
- handleSubmitメソッド内で、指定された上限数に基づいて会話履歴を切り詰めるように変更します。
handleSubmit = async (event) => { event.preventDefault(); const newMessage = { role: "user", content: this.state.input }; this.setState((prevState) => ({ messages: [...prevState.messages, newMessage], input: "", loading: true, })); const { input, system, temperature, max_tokens, messages, max_conversation_length } = this.state; const conversation = [...messages, newMessage].slice(-max_conversation_length); google.script.run .withSuccessHandler((response) => { this.setState((prevState) => ({ messages: [...prevState.messages, { role: "assistant", content: response }], loading: false, })); }) .sendMessageToChatGPT(conversation, system, temperature, max_tokens); };
これで、Advanced設定で会話の上限数を設定できるようになりました。handleSubmitメソッド内のconversation配列が、指定された上限数に基づいて切り詰められています。
あなた:
すばらしい。role:assistantのテキストがMarkdown形式のテキストだったら、HTMLに変換して表示することはできますか?
3 / 3ChatGPT:
はい、それが可能です。Markdown形式のテキストをHTMLに変換するには、react-markdownというライブラリを使用することができます。このライブラリを使用するには、次の手順に従ってください。
- まず、react-markdownをインポートするために、タグの中に以下のスクリプトタグを追加してください。
<script src="https://unpkg.com/react-markdown@5/umd/react-markdown.min.js"></script>
- レンダリング部分で、role: "assistant"のメッセージに対してReactMarkdownコンポーネントを使用してHTMLに変換します。
{messages.map((message, index) => ( <p key={index} style={{ whiteSpace: 'pre-wrap' }}> <strong>{message.role === "user" ? "User" : "ChatGPT"}:</strong> <br /> {message.role === "user" ? ( message.content ) : ( <ReactMarkdown>{message.content}</ReactMarkdown> )} </p> ))}
この変更により、role: "assistant"のメッセージがMarkdown形式である場合、それらがHTMLに変換されて表示されます。ただし、このライブラリは、インターネット接続が必要であるため、オフライン環境では機能しません。また、react-markdownが新しいバージョンに更新されると、このコードが動作しなくなる可能性があります。その場合は、適切なバージョンのreact-markdownを読み込むようにスクリプトタグを更新してください。
あなた:
https://unpkg.com/react-markdown@4.3.1/umd/react-markdown.js が正しいです。
ChatGPT:
失礼しました。指定されたバージョンのreact-markdownをインポートするために、以下のスクリプトタグを
<head>
タグの中に追加してください。<script src="https://unpkg.com/react-markdown@4.3.1/umd/react-markdown.js"></script>
そして、メッセージのレンダリング部分を次のように変更して、role: "assistant"のメッセージがMarkdown形式の場合にHTMLに変換されるようにします。
{messages.map((message, index) => ( <p key={index} style={{ whiteSpace: 'pre-wrap' }}> <strong>{message.role === "user" ? "User" : "ChatGPT"}:</strong> <br /> {message.role === "user" ? ( message.content ) : ( <ReactMarkdown source={message.content} /> )} </p> ))}
これで、role: "assistant"のメッセージがMarkdown形式である場合、それらがHTMLに変換されて表示されます。react-markdown v4.3.1を使用しているため、オフライン環境での動作や、将来的なバージョンアップによる互換性の問題に対処できます。