当前位置:   article > 正文

搭建简单的GPT聊天机器人_gpt搭建聊天机器人

gpt搭建聊天机器人

目录

第一步

进行语料库读取、文本预处理,完成data_utls.py

第二步

进行Seq2Seq模型的构建,完成Seq2Seq.py

第三步

进行模型参数设置、加载词典和数据、数据准备、GPU设置、构建优化器和损失函数,进行模型的训练和测试,完成execute.py。

第四步

通过给出的前端代码,调用flask前端进行测试,完成app.py文件,使用网页端聊天机器人聊天对话。


有问题后台私信

第一步

进行语料库读取、文本预处理,完成data_utls.py

data_utls.py

  1. '''
  2. 数据处理器
  3. '''
  4. import os
  5. import jieba
  6. from tkinter import _flatten
  7. import json
  8. # 读取语料库文件
  9. def read_corpus(corpus_path='../data/dialog/'):
  10. '''
  11. corpus_path:读取文件的路径
  12. '''
  13. corpus_files = os.listdir(corpus_path) # 列出文件路径下所有文件
  14. corpus = []
  15. for corpus_file in corpus_files: # 循环读取各个文件内容
  16. with open(os.path.join(corpus_path, corpus_file), 'r', encoding='utf-8') as f:
  17. corpus.extend(f.readlines())
  18. corpus = [i.replace('\n', '') for i in corpus]
  19. return corpus # 返回语料库的列表数据
  20. print('语料库读取完成!'.center(30, '='))
  21. corpus = read_corpus(corpus_path='../data/dialog/')
  22. print('语料库展示: \n', corpus[:6])
  23. # 分词
  24. def word_cut(corpus, userdict='../data/ids/mydict.txt'):
  25. '''
  26. corpus:语料
  27. userdict:自定义词典
  28. '''
  29. jieba.load_userdict(userdict) # 加载自定义词典
  30. corpus_cut = [jieba.lcut(i) for i in corpus] # 分词
  31. print('分词完成'.center(30, '='))
  32. return corpus_cut
  33. # 构建词典
  34. def get_dict(corpus_cut):
  35. '''
  36. corpus_cut:分词后的语料文件
  37. '''
  38. tmp = _flatten(corpus_cut) # 将分词结果列表拉直
  39. all_dict = list(set(tmp)) # 去除重复词,保留所有出现的唯一的词
  40. id2words = {i: j for i, j in enumerate(all_dict)}
  41. words2id = dict(zip(id2words.values(), id2words.keys())) # 构建词典
  42. print('词典构建完成'.center(30, '='))
  43. return all_dict, id2words, words2id
  44. # 执行分词
  45. corpus_cut = word_cut(corpus, userdict='../data/ids/mydict.txt')
  46. print('分词结果展示: \n', corpus_cut[:2])
  47. # 获取字典
  48. all_dict, id2words, words2id = get_dict(corpus_cut)
  49. print('词典展示: \n', all_dict[:6])
  50. # 文件保存
  51. def save(all_dict, corpus_cut, file_path='../tmp'):
  52. '''
  53. all_dict: 获取的词典
  54. file_path: 文件保存路径
  55. corpus_cut: 分词后的语料文件
  56. '''
  57. if not os.path.exists(file_path):
  58. os.makedirs(file_path) # 如果文件夹不存在则新建
  59. source = corpus_cut[::2] # 问
  60. target = corpus_cut[1::2] # 答
  61. # 构建文件的对应字典
  62. file = {'all_dict.txt': all_dict, 'source.txt': source, 'target.txt': target}
  63. # 分别进行文件处理并保存
  64. for i in file.keys():
  65. if i in ['all_dict.txt']:
  66. with open(os.path.join(file_path, i), 'w', encoding='utf-8') as f:
  67. f.writelines(['\n'.join(file[i])])
  68. else:
  69. with open(os.path.join(file_path, i), 'w', encoding='utf-8') as f:
  70. f.writelines([' '.join(i) + '\n' for i in file[i]])
  71. print('文件已保存'.center(30, '='))
  72. # 执行保存
  73. save(all_dict, corpus_cut, file_path='../tmp')

第二步

进行Seq2Seq模型的构建,完成Seq2Seq.py

  1. import tensorflow as tf
  2. import typing
  3. # 编码
  4. class Encoder(tf.keras.Model):
  5. # 设置参数
  6. def __init__(self, vocab_size: int, embedding_dim: int, enc_units: int) -> None:
  7. '''
  8. vocab_size: 词库大小
  9. embedding_dim: 词向量维度
  10. enc_units: LSTM层的神经元数量
  11. '''
  12. super(Encoder, self).__init__()
  13. self.enc_units = enc_units
  14. # 词嵌入层
  15. self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
  16. # LSTM层,GRU是简单的LSTM层
  17. self.gru = tf.keras.layers.GRU(self.enc_units, return_sequences=True, return_state=True)
  18. # 定义神经网络的传输顺序
  19. def call(self, x: tf.Tensor, **kwargs) -> typing.Tuple[tf.Tensor, tf.Tensor]:
  20. '''
  21. x: 输入的文本
  22. '''
  23. x = self.embedding(x)
  24. output, state = self.gru(x)
  25. return output, state # 输出预测结果和当前状态
  26. # 注意力机制
  27. class BahdanauAttention(tf.keras.Model):
  28. # 设置参数
  29. def __init__(self, units: int) -> None:
  30. '''
  31. units: 神经元数据量
  32. '''
  33. super(BahdanauAttention, self).__init__()
  34. self.W1 = tf.keras.layers.Dense(units) # 全连接层
  35. self.W2 = tf.keras.layers.Dense(units) # 全连接层
  36. self.V = tf.keras.layers.Dense(1) # 输出层
  37. # 设置注意力的计算方式
  38. def call(self, query: tf.Tensor, values: tf.Tensor, **kwargs) -> typing.Tuple[tf.Tensor, tf.Tensor]:
  39. '''
  40. query: 上一层输出的特征值
  41. values: 上一层输出的计算结果
  42. '''
  43. # 维度增加一维
  44. hidden_with_time_axis = tf.expand_dims(query, 1)
  45. # 构造计算方法
  46. score = self.V(tf.nn.tanh(self.W1(values) + self.W2(hidden_with_time_axis)))
  47. # 计算权重
  48. attention_weights = tf.nn.softmax(score, axis=1)
  49. # 计算输出
  50. context_vector = attention_weights * values
  51. context_vector = tf.reduce_sum(context_vector, axis=1)
  52. return context_vector, attention_weights # 输出特征向量和权重
  53. # 解码
  54. class Decoder(tf.keras.Model):
  55. # 设置参数
  56. def __init__(self, vocab_size: int, embedding_dim: int, dec_units: int):
  57. '''
  58. vocab_size: 词库大小
  59. embedding_dim: 词向量维度
  60. dec_units: LSTM层的神经元数量
  61. '''
  62. super(Decoder, self).__init__()
  63. self.dec_units = dec_units
  64. # 词嵌入层
  65. self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
  66. # 添加LSTM层
  67. self.gru = tf.keras.layers.GRU(self.dec_units, return_sequences=True, return_state=True)
  68. # 全连接层
  69. self.fc = tf.keras.layers.Dense(vocab_size)
  70. # 添加注意力机制
  71. self.attention = BahdanauAttention(self.dec_units)
  72. # 设置神经网络传输顺序
  73. def call(self, x: tf.Tensor, hidden: tf.Tensor, enc_output: tf.Tensor) \
  74. -> typing.Tuple[tf.Tensor, tf.Tensor, tf.Tensor]:
  75. '''
  76. x: 输入的文本
  77. hidden: 上一层输出的特征值
  78. enc_output: 上一层输出的计算结果
  79. '''
  80. # 计算注意力机制层的结果
  81. context_vector, attention_weights = self.attention(hidden, enc_output)
  82. # 次嵌入层
  83. x = self.embedding(x)
  84. # 词嵌入结果和注意力机制的结果合并
  85. x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
  86. # 添加注意力机制
  87. output, state = self.gru(x)
  88. # 输出结果更新维度
  89. output = tf.reshape(output, (-1, output.shape[2]))
  90. # 输出层
  91. x = self.fc(output)
  92. return x, state, attention_weights # 输出预测结果,当前状态和权重

第三步

进行模型参数设置、加载词典和数据、数据准备、GPU设置、构建优化器和损失函数,进行模型的训练和测试,完成execute.py。

  1. import os
  2. import datetime
  3. from Seq2Seq import Encoder, Decoder
  4. import tensorflow as tf
  5. # 设置参数
  6. data_path = '../data/ids' # 文件路径
  7. epoch = 100 # 迭代训练次数
  8. batch_size = 15 # 每批次样本数
  9. embedding_dim = 256 # 词嵌入维度
  10. hidden_dim = 512 # 隐层神经元个数
  11. shuffle_buffer_size = 4 # 清洗数据集时将缓冲的实例数
  12. device = -1 # 使用的设备ID,-1即不使用GPU
  13. checkpoint_path = '../tmp/model' # 模型参数保存的路径
  14. MAX_LENGTH = 50 # 句子的最大词长
  15. CONST = {'_BOS': 0, '_EOS': 1, '_PAD': 2, '_UNK': 3}# 最大输出句子的长度
  16. # 加载词典
  17. print(f'[{datetime.datetime.now()}] 加载词典...')
  18. data_path = '../data/ids'
  19. CONST = {'_BOS': 0, '_EOS': 1, '_PAD': 2, '_UNK': 3}
  20. table = tf.lookup.StaticHashTable( # 初始化后即不可变的通用哈希表。
  21. initializer=tf.lookup.TextFileInitializer(
  22. os.path.join(data_path, 'all_dict.txt'),
  23. tf.string,
  24. tf.lookup.TextFileIndex.WHOLE_LINE,
  25. tf.int64,
  26. tf.lookup.TextFileIndex.LINE_NUMBER
  27. ), # 要使用的表初始化程序。有关支持的键和值类型,请参见HashTable内核。
  28. default_value=CONST['_UNK'] - len(CONST) # 表中缺少键时使用的值。
  29. )
  30. # 加载数据
  31. print(f'[{datetime.datetime.now()}] 加载预处理后的数据...')
  32. # 构造序列化的键值对字典
  33. def to_tmp(text):
  34. '''
  35. text: 文本
  36. '''
  37. tokenized = tf.strings.split(tf.reshape(text, [1]), sep=' ')
  38. tmp = table.lookup(tokenized.values) + len(CONST)
  39. return tmp
  40. # 增加开始和结束标记
  41. def add_start_end_tokens(tokens):
  42. '''
  43. tokens: 列化的键值对字典
  44. '''
  45. tmp = tf.concat([[CONST['_BOS']], tf.cast(tokens, tf.int32), [CONST['_EOS']]], axis=0)
  46. return tmp
  47. # 获取数据
  48. def get_dataset(src_path: str, table: tf.lookup.StaticHashTable) -> tf.data.Dataset:
  49. '''
  50. src_path: 文件路径
  51. table:初始化后不可变的通用哈希表。
  52. '''
  53. dataset = tf.data.TextLineDataset(src_path)
  54. dataset = dataset.map(to_tmp)
  55. dataset = dataset.map(add_start_end_tokens)
  56. return dataset
  57. # 获取数据
  58. src_train = get_dataset(os.path.join(data_path, 'source.txt'), table)
  59. tgt_train = get_dataset(os.path.join(data_path, 'target.txt'), table)
  60. # 把数据和特征构造为tf数据集
  61. train_dataset = tf.data.Dataset.zip((src_train, tgt_train))
  62. # 过滤数据实例数
  63. def filter_instance_by_max_length(src: tf.Tensor, tgt: tf.Tensor) -> tf.Tensor:
  64. '''
  65. src: 特征
  66. tgt: 标签
  67. '''
  68. return tf.logical_and(tf.size(src) <= MAX_LENGTH, tf.size(tgt) <= MAX_LENGTH)
  69. train_dataset = train_dataset.filter(filter_instance_by_max_length) # 过滤数据
  70. train_dataset = train_dataset.shuffle(shuffle_buffer_size) # 打乱数据
  71. train_dataset = train_dataset.padded_batch( # 将数据长度变为一致,长度不足用_PAD补齐
  72. batch_size,
  73. padded_shapes=([MAX_LENGTH + 2], [MAX_LENGTH + 2]),
  74. padding_values=(CONST['_PAD'], CONST['_PAD']),
  75. drop_remainder=True,
  76. )
  77. # 提升产生下一个批次数据的效率
  78. train_dataset = train_dataset.prefetch(tf.data.experimental.AUTOTUNE)
  79. # 模型参数保存的路径如果不存在则新建
  80. if not os.path.exists(checkpoint_path):
  81. os.makedirs(checkpoint_path)
  82. # 获得当前主机上GPU运算设备的列表
  83. gpus = tf.config.experimental.list_physical_devices('GPU')
  84. if 0 <= device and 0 < len(gpus):
  85. # 限制TensorFlow仅使用指定的GPU
  86. tf.config.experimental.set_visible_devices(gpus[device], 'GPU')
  87. logical_gpus = tf.config.experimental.list_logical_devices('GPU')
  88. # 建模
  89. print(f'[{datetime.datetime.now()}] 创建一个seq2seq模型...')
  90. encoder = Encoder(table.size().numpy() + len(CONST), embedding_dim, hidden_dim)
  91. decoder = Decoder(table.size().numpy() + len(CONST), embedding_dim, hidden_dim)
  92. # 设置优化器
  93. print(f'[{datetime.datetime.now()}] 准备优化器...')
  94. optimizer = tf.keras.optimizers.Adam()
  95. # 设置损失函数
  96. print(f'[{datetime.datetime.now()}] 设置损失函数...')
  97. # 损失值计算方式
  98. loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
  99. # 损失函数
  100. def loss_function(loss_object, real: tf.Tensor, pred: tf.Tensor) -> tf.Tensor:
  101. '''
  102. loss_object: 损失值计算方式
  103. real: 真实值
  104. pred: 预测值
  105. '''
  106. # 计算真实值和预测值的误差
  107. loss_ = loss_object(real, pred)
  108. # 返回输出并不相等的值,并用_PAD填充
  109. mask = tf.math.logical_not(tf.math.equal(real, CONST['_PAD']))
  110. # 数据格式转换为跟损失值一致
  111. mask = tf.cast(mask, dtype=loss_.dtype)
  112. return tf.reduce_mean(loss_ * mask) # 返回平均误差
  113. # 设置模型保存
  114. checkpoint = tf.train.Checkpoint(optimizer=optimizer, encoder=encoder, decoder=decoder)
  115. # 训练
  116. def train_step(src: tf.Tensor, tgt: tf.Tensor):
  117. '''
  118. src: 输入的文本
  119. tgt: 标签
  120. '''
  121. # 获取标签维度
  122. tgt_width, tgt_length = tgt.shape
  123. loss = 0
  124. # 创建梯度带,用于反向计算导数
  125. with tf.GradientTape() as tape:
  126. # 对输入的文本编码
  127. enc_output, enc_hidden = encoder(src)
  128. # 设置解码的神经元数目与编码的神经元数目相等
  129. dec_hidden = enc_hidden
  130. # 根据标签对数据解码
  131. for t in range(tgt_length - 1):
  132. # 更新维度,新增1
  133. dec_input = tf.expand_dims(tgt[:, t], 1)
  134. # 解码
  135. predictions, dec_hidden, dec_out = decoder(dec_input, dec_hidden, enc_output)
  136. # 计算损失值
  137. loss += loss_function(loss_object, tgt[:, t + 1], predictions)
  138. # 计算一次训练的平均损失值
  139. batch_loss = loss / tgt_length
  140. # 更新预测值
  141. variables = encoder.trainable_variables + decoder.trainable_variables
  142. # 反向求导
  143. gradients = tape.gradient(loss, variables)
  144. # 利用优化器更新权重
  145. optimizer.apply_gradients(zip(gradients, variables))
  146. return batch_loss # 返回每次迭代训练的损失值
  147. print(f'[{datetime.datetime.now()}] 开始训练模型...')
  148. # 根据设定的训练次数去训练模型
  149. for ep in range(epoch):
  150. # 设置损失值
  151. total_loss = 0
  152. # 将每批次的数据取出,放入模型里
  153. for batch, (src, tgt) in enumerate(train_dataset):
  154. # 训练并计算损失值
  155. batch_loss = train_step(src, tgt)
  156. total_loss += batch_loss
  157. if ep % 100 == 0:
  158. # 每100训练次保存一次模型
  159. checkpoint_prefix = os.path.join(checkpoint_path, 'ckpt')
  160. checkpoint.save(file_prefix=checkpoint_prefix)
  161. print(f'[{datetime.datetime.now()}] 迭代次数: {ep+1} 损失值: {total_loss:.4f}')
  162. # 模型预测
  163. def predict(sentence='你好'):
  164. # 导入训练参数
  165. checkpoint.restore(tf.train.latest_checkpoint(checkpoint_path))
  166. # 给句子添加开始和结束标记
  167. sentence = '_BOS' + sentence + '_EOS'
  168. # 读取字段
  169. with open(os.path.join(data_path, 'all_dict.txt'), 'r', encoding='utf-8') as f:
  170. all_dict = f.read().split()
  171. # 构建: 词-->id的映射字典
  172. word2id = {j: i+len(CONST) for i, j in enumerate(all_dict)}
  173. word2id.update(CONST)
  174. # 构建: id-->词的映射字典
  175. id2word = dict(zip(word2id.values(), word2id.keys()))
  176. # 分词时保留_EOS 和 _BOS
  177. from jieba import lcut, add_word
  178. for i in ['_EOS', '_BOS']:
  179. add_word(i)
  180. # 添加识别不到的词,用_UNK表示
  181. inputs = [word2id.get(i, CONST['_UNK']) for i in lcut(sentence)]
  182. # 长度填充
  183. inputs = tf.keras.preprocessing.sequence.pad_sequences(
  184. [inputs], maxlen=MAX_LENGTH, padding='post', value=CONST['_PAD'])
  185. # 将数据转为tensorflow的数据类型
  186. inputs = tf.convert_to_tensor(inputs)
  187. # 空字符串,用于保留预测结果
  188. result = ''
  189. # 编码
  190. enc_out, enc_hidden = encoder(inputs)
  191. dec_hidden = enc_hidden
  192. dec_input = tf.expand_dims([word2id['_BOS']], 0)
  193. for t in range(MAX_LENGTH):
  194. # 解码
  195. predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out)
  196. # 预测出词语对应的id
  197. predicted_id = tf.argmax(predictions[0]).numpy()
  198. # 通过字典的映射,用id寻找词,遇到_EOS停止输出
  199. if id2word.get(predicted_id, '_UNK') == '_EOS':
  200. break
  201. # 未预测出来的词用_UNK替代
  202. result += id2word.get(predicted_id, '_UNK')
  203. dec_input = tf.expand_dims([predicted_id], 0)
  204. return result # 返回预测结果
  205. print('预测示例: \n', predict(sentence='你好,在吗'))

第四步

通过给出的前端代码,调用flask前端进行测试,完成app.py文件,使用网页端聊天机器人聊天对话。

app.py代码

  1. import tensorflow as tf
  2. import os
  3. from Seq2Seq import Encoder, Decoder
  4. from jieba import lcut, add_word
  5. from flask import Flask, render_template, request, jsonify
  6. # 设置参数
  7. data_path = '../data/ids' # 数据路径
  8. embedding_dim = 256 # 词嵌入维度
  9. hidden_dim = 512 # 隐层神经元个数
  10. checkpoint_path = '../tmp/model' # 模型参数保存的路径
  11. MAX_LENGTH = 50 # 句子的最大词长
  12. CONST = {'_BOS': 0, '_EOS': 1, '_PAD': 2, '_UNK': 3}
  13. # 聊天预测
  14. def chat(sentence='你好'):
  15. # 初始化所有词语的哈希表
  16. table = tf.lookup.StaticHashTable( # 初始化后即不可变的通用哈希表。
  17. initializer=tf.lookup.TextFileInitializer(
  18. os.path.join(data_path, 'all_dict.txt'),
  19. tf.string,
  20. tf.lookup.TextFileIndex.WHOLE_LINE,
  21. tf.int64,
  22. tf.lookup.TextFileIndex.LINE_NUMBER
  23. ), # 要使用的表初始化程序。有关支持的键和值类型,请参见HashTable内核。
  24. default_value=CONST['_UNK'] - len(CONST) # 表中缺少键时使用的值。
  25. )
  26. # 实例化编码器和解码器
  27. encoder = Encoder(table.size().numpy() + len(CONST), embedding_dim, hidden_dim)
  28. decoder = Decoder(table.size().numpy() + len(CONST), embedding_dim, hidden_dim)
  29. optimizer = tf.keras.optimizers.Adam() # 优化器
  30. # 模型保存路径
  31. checkpoint = tf.train.Checkpoint(optimizer=optimizer, encoder=encoder, decoder=decoder)
  32. # 导入训练参数
  33. checkpoint.restore(tf.train.latest_checkpoint(checkpoint_path))
  34. # 给句子添加开始和结束标记
  35. sentence = '_BOS' + sentence + '_EOS'
  36. # 读取字段
  37. with open(os.path.join(data_path, 'all_dict.txt'), 'r', encoding='utf-8') as f:
  38. all_dict = f.read().split()
  39. # 构建: 词-->id的映射字典
  40. word2id = {j: i + len(CONST) for i, j in enumerate(all_dict)}
  41. word2id.update(CONST)
  42. # 构建: id-->词的映射字典
  43. id2word = dict(zip(word2id.values(), word2id.keys()))
  44. # 分词时保留_EOS 和 _BOS
  45. for i in ['_EOS', '_BOS']:
  46. add_word(i)
  47. # 添加识别不到的词,用_UNK表示
  48. inputs = [word2id.get(i, CONST['_UNK']) for i in lcut(sentence)]
  49. # 长度填充
  50. inputs = tf.keras.preprocessing.sequence.pad_sequences(
  51. [inputs], maxlen=MAX_LENGTH, padding='post', value=CONST['_PAD'])
  52. # 将数据转为tensorflow的数据类型
  53. inputs = tf.convert_to_tensor(inputs)
  54. # 空字符串,用于保留预测结果
  55. result = ''
  56. # 编码
  57. enc_out, enc_hidden = encoder(inputs)
  58. dec_hidden = enc_hidden
  59. dec_input = tf.expand_dims([word2id['_BOS']], 0)
  60. for t in range(MAX_LENGTH):
  61. # 解码
  62. predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out)
  63. # 预测出词语对应的id
  64. predicted_id = tf.argmax(predictions[0]).numpy()
  65. # 通过字典的映射,用id寻找词,遇到_EOS停止输出
  66. if id2word.get(predicted_id, '_UNK') == '_EOS':
  67. break
  68. # 未预测出来的词用_UNK替代
  69. result += id2word.get(predicted_id, '_UNK')
  70. dec_input = tf.expand_dims([predicted_id], 0)
  71. return result # 返回预测结果
  72. # 实例化APP
  73. app = Flask(__name__, static_url_path='/static')
  74. @app.route('/message', methods=['POST'])
  75. # 定义应答函数,用于获取输入信息并返回相应的答案
  76. def reply():
  77. # 从请求中获取参数信息
  78. req_msg = request.form['msg']
  79. # 将语句使用结巴分词进行分词
  80. # req_msg = " ".join(jieba.cut(req_msg))
  81. # 调用decode_line对生成回答信息
  82. res_msg = chat(req_msg)
  83. # 将unk值的词用微笑符号代替
  84. res_msg = res_msg.replace('_UNK', '^_^')
  85. res_msg = res_msg.strip()
  86. # 如果接受到的内容为空,则给出相应的回复
  87. if res_msg == ' ':
  88. res_msg = '我们来聊聊天吧'
  89. return jsonify({'text': res_msg})
  90. @app.route("/")
  91. # 在网页上展示对话
  92. def index():
  93. return render_template('index.html')
  94. # 启动APP
  95. if (__name__ == '__main__'):
  96. app.run(host='127.0.0.1', port=8808)

页面代码

  1. <!DOCTYPE html>
  2. <html >
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>TIPDM</title>
  6. <link rel="stylesheet" href="static/css/normalize.css">
  7. <link rel='stylesheet prefetch' href='https://fonts.googleapis.com/css?family=Open+Sans'>
  8. <link rel='stylesheet prefetch' href='/static/js/jquery.mCustomScrollbar.min.css'>
  9. <link rel="stylesheet" href="static/css/style.css">
  10. <script src="/static/js/jquery-latest.js"></script>
  11. </head>
  12. <body onload="init()" id="page_body">
  13. <div class="chat">
  14. <div class="chat-title">
  15. <h1>中文聊天机器人</h1>
  16. <h2>咱们唠个五毛钱的天呗</h2>
  17. <figure class="avatar">
  18. <img src="static/res/7.png" /></figure>
  19. </div>
  20. <div class="messages">
  21. <div class="messages-content"></div>
  22. </div>
  23. <div class="message-box">
  24. <textarea type="text" class="message-input" placeholder="Type message..."></textarea>
  25. <button type="submit" class="message-submit">Send</button>
  26. </div>
  27. </div>
  28. <div class="bg"></div>
  29. <script src='/static/js/jquery.min.js'></script>
  30. <script src='/static/js/jquery.mCustomScrollbar.concat.min.js'></script>
  31. <script src="static/js/index.js"></script>
  32. </body>
  33. </html>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/257353?site
推荐阅读
相关标签
  

闽ICP备14008679号