mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: add copy code btn in chat page, fixed bug for code wrap in model of ChatGLM and XunFei
This commit is contained in:
		@@ -132,19 +132,20 @@ type ModelAPIConfig struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SystemConfig struct {
 | 
			
		||||
	Title           string   `json:"title"`
 | 
			
		||||
	AdminTitle      string   `json:"admin_title"`
 | 
			
		||||
	Models          []string `json:"models"`
 | 
			
		||||
	UserInitCalls   int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
	InitImgCalls    int      `json:"init_img_calls"`
 | 
			
		||||
	VipMonthCalls   int      `json:"vip_month_calls"` // 会员每个赠送的调用次数
 | 
			
		||||
	EnabledRegister bool     `json:"enabled_register"`
 | 
			
		||||
	EnabledMsg      bool     `json:"enabled_msg"`       // 启用短信验证码服务
 | 
			
		||||
	EnabledDraw     bool     `json:"enabled_draw"`      // 启动 AI 绘画功能
 | 
			
		||||
	RewardImg       string   `json:"reward_img"`        // 众筹收款二维码地址
 | 
			
		||||
	EnabledFunction bool     `json:"enabled_function"`  // 启用 API 函数功能
 | 
			
		||||
	EnabledReward   bool     `json:"enabled_reward"`    // 启用众筹功能
 | 
			
		||||
	EnabledAlipay   bool     `json:"enabled_alipay"`    // 是否启用支付宝支付通道
 | 
			
		||||
	OrderPayTimeout int      `json:"order_pay_timeout"` //订单支付超时时间
 | 
			
		||||
	DefaultModels   []string `json:"default_models"`    // 默认开通的 AI 模型
 | 
			
		||||
	Title            string   `json:"title"`
 | 
			
		||||
	AdminTitle       string   `json:"admin_title"`
 | 
			
		||||
	Models           []string `json:"models"`
 | 
			
		||||
	UserInitCalls    int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
	InitImgCalls     int      `json:"init_img_calls"`
 | 
			
		||||
	VipMonthCalls    int      `json:"vip_month_calls"` // 会员每个赠送的调用次数
 | 
			
		||||
	EnabledRegister  bool     `json:"enabled_register"`
 | 
			
		||||
	EnabledMsg       bool     `json:"enabled_msg"`         // 启用短信验证码服务
 | 
			
		||||
	EnabledDraw      bool     `json:"enabled_draw"`        // 启动 AI 绘画功能
 | 
			
		||||
	RewardImg        string   `json:"reward_img"`          // 众筹收款二维码地址
 | 
			
		||||
	EnabledFunction  bool     `json:"enabled_function"`    // 启用 API 函数功能
 | 
			
		||||
	EnabledReward    bool     `json:"enabled_reward"`      // 启用众筹功能
 | 
			
		||||
	EnabledAlipay    bool     `json:"enabled_alipay"`      // 是否启用支付宝支付通道
 | 
			
		||||
	OrderPayTimeout  int      `json:"order_pay_timeout"`   //订单支付超时时间
 | 
			
		||||
	DefaultModels    []string `json:"default_models"`      // 默认开通的 AI 模型
 | 
			
		||||
	OrderPayInfoText string   `json:"order_pay_info_text"` // 订单支付页面说明文字
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
@@ -22,10 +23,10 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statsVo struct {
 | 
			
		||||
	Users   int64   `json:"users"`
 | 
			
		||||
	Chats   int64   `json:"chats"`
 | 
			
		||||
	Tokens  int64   `json:"tokens"`
 | 
			
		||||
	Rewards float64 `json:"rewards"`
 | 
			
		||||
	Users  int64   `json:"users"`
 | 
			
		||||
	Chats  int64   `json:"chats"`
 | 
			
		||||
	Tokens int     `json:"tokens"`
 | 
			
		||||
	Income float64 `json:"income"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
@@ -47,17 +48,24 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// tokens took stats
 | 
			
		||||
	var tokenCount int64
 | 
			
		||||
	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Tokens = tokenCount
 | 
			
		||||
	var historyMessages []model.HistoryMessage
 | 
			
		||||
	res = h.db.Where("created_at > ?", zeroTime).Find(&historyMessages)
 | 
			
		||||
	for _, item := range historyMessages {
 | 
			
		||||
		stats.Tokens += item.Tokens
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reward revenue
 | 
			
		||||
	var amount float64
 | 
			
		||||
	res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Rewards = amount
 | 
			
		||||
	// 众筹收入
 | 
			
		||||
	var rewards []model.Reward
 | 
			
		||||
	res = h.db.Where("created_at > ?", zeroTime).Find(&rewards)
 | 
			
		||||
	for _, item := range rewards {
 | 
			
		||||
		stats.Income += item.Amount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 订单收入
 | 
			
		||||
	var orders []model.Order
 | 
			
		||||
	res = h.db.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
 | 
			
		||||
	for _, item := range orders {
 | 
			
		||||
		stats.Income += item.Amount
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, stats)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,11 @@ func (h *ChatHandler) sendBaiduMessage(
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 处理代码换行
 | 
			
		||||
			if len(content) == 0 {
 | 
			
		||||
				content = "\n"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var resp baiduResp
 | 
			
		||||
			err := utils.JsonDecode(content, &resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,10 @@ func (h *ChatHandler) sendChatGLMMessage(
 | 
			
		||||
			if strings.HasPrefix(line, "data:") {
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
			// 处理代码换行
 | 
			
		||||
			if len(content) == 0 {
 | 
			
		||||
				content = "\n"
 | 
			
		||||
			}
 | 
			
		||||
			switch event {
 | 
			
		||||
			case "add":
 | 
			
		||||
				if len(contents) == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -138,6 +138,10 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content = result.Payload.Choices.Text[0].Content
 | 
			
		||||
		// 处理代码换行
 | 
			
		||||
		if len(content) == 0 {
 | 
			
		||||
			content = "\n"
 | 
			
		||||
		}
 | 
			
		||||
		contents = append(contents, content)
 | 
			
		||||
		// 第一个结果
 | 
			
		||||
		if result.Payload.Choices.Status == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,11 +31,11 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {defineComponent} from "vue"
 | 
			
		||||
import {Clock, DocumentCopy} from "@element-plus/icons-vue";
 | 
			
		||||
import {Clock, DocumentCopy, Position} from "@element-plus/icons-vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'ChatReply',
 | 
			
		||||
  components: {Clock, DocumentCopy},
 | 
			
		||||
  components: {Position, Clock, DocumentCopy},
 | 
			
		||||
  props: {
 | 
			
		||||
    content: {
 | 
			
		||||
      type: String,
 | 
			
		||||
@@ -126,6 +126,39 @@ export default defineComponent({
 | 
			
		||||
            margin-top 0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .code-container {
 | 
			
		||||
            position relative
 | 
			
		||||
 | 
			
		||||
            .hljs {
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              line-height 1.5
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .copy-code-btn {
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              right 10px
 | 
			
		||||
              top 10px
 | 
			
		||||
              cursor pointer
 | 
			
		||||
              font-size 12px
 | 
			
		||||
              color #c1c1c1
 | 
			
		||||
 | 
			
		||||
              &:hover {
 | 
			
		||||
                color #20a0ff
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .lang-name {
 | 
			
		||||
            position absolute;
 | 
			
		||||
            right 10px
 | 
			
		||||
            bottom 50px
 | 
			
		||||
            padding 2px 6px 4px 6px
 | 
			
		||||
            background-color #444444
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
            color #00e0e0
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // 设置表格边框
 | 
			
		||||
 | 
			
		||||
          table {
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@
 | 
			
		||||
              <span>导出会话</span>
 | 
			
		||||
            </el-button>
 | 
			
		||||
 | 
			
		||||
            <el-button type="warning" @click="showFeekbackDialog = true">
 | 
			
		||||
            <el-button type="warning" @click="showFeedbackDialog = true">
 | 
			
		||||
              <el-icon>
 | 
			
		||||
                <Promotion/>
 | 
			
		||||
              </el-icon>
 | 
			
		||||
@@ -202,15 +202,16 @@
 | 
			
		||||
    </el-container>
 | 
			
		||||
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        v-model="showFeekbackDialog"
 | 
			
		||||
        v-model="showFeedbackDialog"
 | 
			
		||||
        :show-close="true"
 | 
			
		||||
        width="300px"
 | 
			
		||||
        width="340px"
 | 
			
		||||
        title="意见反馈"
 | 
			
		||||
    >
 | 
			
		||||
      <el-alert type="info" :closable="false">
 | 
			
		||||
        <div style="font-size: 14px">
 | 
			
		||||
          如果您对本项目有任何改进意见,您可以通过 Github
 | 
			
		||||
          <el-link style="color: #f56c6c; font-weight: bold;" href="https://github.com/yangjian102621/chatgpt-plus">
 | 
			
		||||
          <el-link style="color: #f56c6c; font-weight: bold;"
 | 
			
		||||
                   href="https://github.com/yangjian102621/chatgpt-plus/issues">
 | 
			
		||||
            提交改进意见
 | 
			
		||||
          </el-link>
 | 
			
		||||
          或者通过扫描下面的微信二维码加入 AI 技术交流群。
 | 
			
		||||
@@ -277,7 +278,7 @@ const showConfigDialog = ref(false);
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
const showHello = ref(true)
 | 
			
		||||
const textInput = ref(null)
 | 
			
		||||
const showFeekbackDialog = ref(false)
 | 
			
		||||
const showFeedbackDialog = ref(false)
 | 
			
		||||
 | 
			
		||||
if (isMobile()) {
 | 
			
		||||
  router.replace("/mobile")
 | 
			
		||||
@@ -333,7 +334,7 @@ onMounted(() => {
 | 
			
		||||
    router.push('/login')
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const clipboard = new Clipboard('.copy-reply');
 | 
			
		||||
  const clipboard = new Clipboard('.copy-reply, .copy-code-btn');
 | 
			
		||||
  clipboard.on('success', () => {
 | 
			
		||||
    ElMessage.success('复制成功!');
 | 
			
		||||
  })
 | 
			
		||||
@@ -469,6 +470,28 @@ const removeChat = function (event, chat) {
 | 
			
		||||
  curOpt.value = 'remove';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const md = require('markdown-it')({
 | 
			
		||||
  breaks: true,
 | 
			
		||||
  highlight: function (str, lang) {
 | 
			
		||||
    const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
 | 
			
		||||
    // 显示复制代码按钮
 | 
			
		||||
    const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
 | 
			
		||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
 | 
			
		||||
    if (lang && hl.getLanguage(lang)) {
 | 
			
		||||
      const langHtml = `<span class="lang-name">${lang}</span>`
 | 
			
		||||
      // 处理代码高亮
 | 
			
		||||
      const preCode = hl.highlight(lang, str, true).value
 | 
			
		||||
      // 将代码包裹在 pre 中
 | 
			
		||||
      return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 处理代码高亮
 | 
			
		||||
    const preCode = md.utils.escapeHtml(str)
 | 
			
		||||
    // 将代码包裹在 pre 中
 | 
			
		||||
    return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 创建 socket 连接
 | 
			
		||||
const prompt = ref('');
 | 
			
		||||
const showStopGenerate = ref(false); // 停止生成
 | 
			
		||||
@@ -542,7 +565,6 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
        } else if (data.type === "mj") {
 | 
			
		||||
          disableInput(true)
 | 
			
		||||
          const content = data.content;
 | 
			
		||||
          const md = require('markdown-it')({breaks: true});
 | 
			
		||||
          content.html = md.render(content.content)
 | 
			
		||||
          let key = content.key
 | 
			
		||||
          // fixed bug: 执行 Upscale 和 Variation 操作的时候覆盖之前的绘画
 | 
			
		||||
@@ -596,19 +618,9 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
          lineBuffer.value += data.content;
 | 
			
		||||
          const md = require('markdown-it')({breaks: true});
 | 
			
		||||
          const reply = chatData.value[chatData.value.length - 1]
 | 
			
		||||
          reply['orgContent'] = lineBuffer.value;
 | 
			
		||||
          reply['content'] = md.render(lineBuffer.value);
 | 
			
		||||
 | 
			
		||||
          nextTick(() => {
 | 
			
		||||
            hl.configure({ignoreUnescapedHTML: true})
 | 
			
		||||
            const lines = document.querySelectorAll('.chat-line');
 | 
			
		||||
            const blocks = lines[lines.length - 1].querySelectorAll('pre code');
 | 
			
		||||
            blocks.forEach((block) => {
 | 
			
		||||
              hl.highlightElement(block)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        // 将聊天框的滚动条滑动到最底部
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
@@ -748,9 +760,6 @@ const loadChatHistory = function (chatId) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    showHello.value = false
 | 
			
		||||
 | 
			
		||||
    const md = require('markdown-it')({breaks: true});
 | 
			
		||||
    // md.use(require('markdown-it-copy')); // 代码复制功能
 | 
			
		||||
    for (let i = 0; i < data.length; i++) {
 | 
			
		||||
      if (data[i].type === "prompt") {
 | 
			
		||||
        chatData.value.push(data[i]);
 | 
			
		||||
@@ -768,11 +777,6 @@ const loadChatHistory = function (chatId) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      hl.configure({ignoreUnescapedHTML: true})
 | 
			
		||||
      const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
 | 
			
		||||
      blocks.forEach((block) => {
 | 
			
		||||
        hl.highlightElement(block)
 | 
			
		||||
      })
 | 
			
		||||
      document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
 | 
			
		||||
    })
 | 
			
		||||
    loading.value = false
 | 
			
		||||
 
 | 
			
		||||
@@ -33,10 +33,9 @@
 | 
			
		||||
 | 
			
		||||
        <el-col :span="17">
 | 
			
		||||
          <div class="product-box">
 | 
			
		||||
            <div class="info">
 | 
			
		||||
            <div class="info" v-if="orderPayInfoText !== ''">
 | 
			
		||||
              <el-alert type="info" show-icon :closable="false" effect="dark">
 | 
			
		||||
                <strong>说明:</strong> 成为本站会员后每月有500次对话额度,50次 AI 绘画额度,限制下月1号解除,若在期间超过次数后可单独购买点卡。
 | 
			
		||||
                当月充值的点卡有效期可以延期到下个月底。
 | 
			
		||||
                <strong>说明:</strong> {{ orderPayInfoText }}
 | 
			
		||||
              </el-alert>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@@ -176,6 +175,7 @@ const activeOrderNo = ref("")
 | 
			
		||||
const countDown = ref(null)
 | 
			
		||||
const orderTimeout = ref(1800)
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const orderPayInfoText = ref("")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@@ -194,6 +194,7 @@ onMounted(() => {
 | 
			
		||||
  httpGet("/api/admin/config/get?key=system").then(res => {
 | 
			
		||||
    rewardImg.value = res.data['reward_img']
 | 
			
		||||
    enableReward.value = res.data['enabled_reward']
 | 
			
		||||
    orderPayInfoText.value = res.data['order_pay_info_text']
 | 
			
		||||
    if (res.data['order_pay_timeout'] > 0) {
 | 
			
		||||
      orderTimeout.value = res.data['order_pay_timeout']
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@
 | 
			
		||||
              <i class="iconfont icon-reward"></i>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
            <div class="grid-cont-right">
 | 
			
		||||
              <div class="grid-num">¥{{ stats.rewards }}</div>
 | 
			
		||||
              <div class="grid-num">¥{{ stats.income }}</div>
 | 
			
		||||
              <div>今日入账</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -70,7 +70,7 @@ httpGet('/api/admin/dashboard/stats').then((res) => {
 | 
			
		||||
  stats.value.users = res.data.users
 | 
			
		||||
  stats.value.chats = res.data.chats
 | 
			
		||||
  stats.value.tokens = res.data.tokens
 | 
			
		||||
  stats.value.rewards = res.data.rewards
 | 
			
		||||
  stats.value.income = res.data.income
 | 
			
		||||
  loading.value = false
 | 
			
		||||
}).catch((e) => {
 | 
			
		||||
  ElMessage.error("获取统计数据失败:" + e.message)
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,15 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="会员充值说明" prop="order_pay_info_text">
 | 
			
		||||
          <el-input
 | 
			
		||||
              v-model="system['order_pay_info_text']"
 | 
			
		||||
              :autosize="{ minRows: 3, maxRows: 10 }"
 | 
			
		||||
              type="textarea"
 | 
			
		||||
              placeholder="请输入会员充值说明文字,比如介绍会员计划"
 | 
			
		||||
          />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="默认AI模型" prop="default_models">
 | 
			
		||||
          <template #default>
 | 
			
		||||
            <div class="tip-input">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user