mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: chat with file function is ready
This commit is contained in:
		@@ -323,20 +323,46 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
		reqMgs = append(reqMgs, m)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fullPrompt := prompt
 | 
			
		||||
	text := prompt
 | 
			
		||||
	// extract files in prompt
 | 
			
		||||
	files := utils.ExtractFileURLs(prompt)
 | 
			
		||||
	logger.Debugf("detected FILES: %+v", files)
 | 
			
		||||
	if len(files) > 0 {
 | 
			
		||||
		contents := make([]string, 0)
 | 
			
		||||
		var file model.File
 | 
			
		||||
		for _, v := range files {
 | 
			
		||||
			h.DB.Where("url = ?", v).First(&file)
 | 
			
		||||
			content, err := utils.ReadFileContent(v)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
 | 
			
		||||
			}
 | 
			
		||||
			text = strings.Replace(text, v, "", 1)
 | 
			
		||||
		}
 | 
			
		||||
		if len(contents) > 0 {
 | 
			
		||||
			fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
 | 
			
		||||
		if tokens > session.Model.MaxContext {
 | 
			
		||||
			return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug("最终Prompt:", fullPrompt)
 | 
			
		||||
 | 
			
		||||
	if session.Model.Platform == types.QWen.Value {
 | 
			
		||||
		req.Input = make(map[string]interface{})
 | 
			
		||||
		reqMgs = append(reqMgs, types.Message{
 | 
			
		||||
			Role:    "user",
 | 
			
		||||
			Content: prompt,
 | 
			
		||||
			Content: fullPrompt,
 | 
			
		||||
		})
 | 
			
		||||
		req.Input["messages"] = reqMgs
 | 
			
		||||
	} else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model
 | 
			
		||||
		imgURLs := utils.ExtractImgURL(prompt)
 | 
			
		||||
		imgURLs := utils.ExtractImgURLs(prompt)
 | 
			
		||||
		logger.Debugf("detected IMG: %+v", imgURLs)
 | 
			
		||||
		var content interface{}
 | 
			
		||||
		if len(imgURLs) > 0 {
 | 
			
		||||
			data := make([]interface{}, 0)
 | 
			
		||||
			text := prompt
 | 
			
		||||
			for _, v := range imgURLs {
 | 
			
		||||
				text = strings.Replace(text, v, "", 1)
 | 
			
		||||
				data = append(data, gin.H{
 | 
			
		||||
@@ -352,7 +378,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
			})
 | 
			
		||||
			content = data
 | 
			
		||||
		} else {
 | 
			
		||||
			content = prompt
 | 
			
		||||
			content = fullPrompt
 | 
			
		||||
		}
 | 
			
		||||
		req.Messages = append(reqMgs, map[string]interface{}{
 | 
			
		||||
			"role":    "user",
 | 
			
		||||
@@ -361,7 +387,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Messages = append(reqMgs, map[string]interface{}{
 | 
			
		||||
			"role":    "user",
 | 
			
		||||
			"content": prompt,
 | 
			
		||||
			"content": fullPrompt,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -454,7 +480,7 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
 | 
			
		||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
 | 
			
		||||
	// if the chat model bind a KEY, use it directly
 | 
			
		||||
	if session.Model.KeyId > 0 {
 | 
			
		||||
		h.DB.Debug().Where("id", session.Model.KeyId).Where("enabled", true).Find(apiKey)
 | 
			
		||||
		h.DB.Where("id", session.Model.KeyId).Find(apiKey)
 | 
			
		||||
	}
 | 
			
		||||
	// use the last unused key
 | 
			
		||||
	if apiKey.Id == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,7 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
	var res *gorm.DB
 | 
			
		||||
	// use the bind key
 | 
			
		||||
	if session.Model.KeyId > 0 {
 | 
			
		||||
		res = h.DB.Where("id", session.Model.KeyId).Where("enabled", true).Find(&apiKey)
 | 
			
		||||
		res = h.DB.Where("id", session.Model.KeyId).Find(&apiKey)
 | 
			
		||||
	}
 | 
			
		||||
	// use the last unused key
 | 
			
		||||
	if apiKey.Id == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -215,7 +215,7 @@ func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatMod
 | 
			
		||||
	// if the chat model bind a KEY, use it directly
 | 
			
		||||
	var res *gorm.DB
 | 
			
		||||
	if chatModel.KeyId > 0 {
 | 
			
		||||
		res = h.DB.Where("id", chatModel.KeyId).Where("enabled", true).Find(apiKey)
 | 
			
		||||
		res = h.DB.Where("id", chatModel.KeyId).Find(apiKey)
 | 
			
		||||
	}
 | 
			
		||||
	// use the last unused key
 | 
			
		||||
	if apiKey.Id == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import (
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	file := "http://nk.img.r9it.com/chatgpt-plus/1719389335351828.xlsx"
 | 
			
		||||
	content, err := utils.ReadPdf(file)
 | 
			
		||||
	content, err := utils.ReadFileContent(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ import (
 | 
			
		||||
	"github.com/google/go-tika/tika"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ReadPdf(filePath string) (string, error) {
 | 
			
		||||
func ReadFileContent(filePath string) (string, error) {
 | 
			
		||||
	// for remote file, download it first
 | 
			
		||||
	if strings.HasPrefix(filePath, "http") {
 | 
			
		||||
		file, err := downloadFile(filePath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -31,22 +32,34 @@ func ReadPdf(filePath string) (string, error) {
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	// 使用 Tika 提取 PDF 文件的文本内容
 | 
			
		||||
	html, err := client.Parse(context.TODO(), file)
 | 
			
		||||
	content, err := client.Parse(context.TODO(), file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with parse file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println(html)
 | 
			
		||||
 | 
			
		||||
	return cleanBlankLine(html), nil
 | 
			
		||||
	ext := filepath.Ext(filePath)
 | 
			
		||||
	switch ext {
 | 
			
		||||
	case ".doc", ".docx", ".pdf", ".pptx", "ppt":
 | 
			
		||||
		return cleanBlankLine(cleanHtml(content, false)), nil
 | 
			
		||||
	case ".xls", ".xlsx":
 | 
			
		||||
		return cleanBlankLine(cleanHtml(content, true)), nil
 | 
			
		||||
	default:
 | 
			
		||||
		return cleanBlankLine(content), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 清理文本内容
 | 
			
		||||
func cleanHtml(html string) string {
 | 
			
		||||
func cleanHtml(html string, keepTable bool) string {
 | 
			
		||||
	// 清理 HTML 标签
 | 
			
		||||
	p := bluemonday.StrictPolicy()
 | 
			
		||||
	return p.Sanitize(html)
 | 
			
		||||
	var policy *bluemonday.Policy
 | 
			
		||||
	if keepTable {
 | 
			
		||||
		policy = bluemonday.NewPolicy()
 | 
			
		||||
		policy.AllowElements("table", "thead", "tbody", "tfoot", "tr", "td", "th")
 | 
			
		||||
	} else {
 | 
			
		||||
		policy = bluemonday.StrictPolicy()
 | 
			
		||||
	}
 | 
			
		||||
	return policy.Sanitize(html)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cleanBlankLine(content string) string {
 | 
			
		||||
@@ -57,6 +70,12 @@ func cleanBlankLine(content string) string {
 | 
			
		||||
		if len(line) < 2 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// discard image
 | 
			
		||||
		if strings.HasSuffix(line, ".png") ||
 | 
			
		||||
			strings.HasSuffix(line, ".jpg") ||
 | 
			
		||||
			strings.HasSuffix(line, ".jpeg") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		texts = append(texts, line)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,7 +88,7 @@ func GetImgExt(filename string) string {
 | 
			
		||||
	return ext
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ExtractImgURL(text string) []string {
 | 
			
		||||
func ExtractImgURLs(text string) []string {
 | 
			
		||||
	re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:png|jpg|jpeg|gif))`)
 | 
			
		||||
	matches := re.FindAllStringSubmatch(text, 10)
 | 
			
		||||
	urls := make([]string, 0)
 | 
			
		||||
@@ -99,3 +99,15 @@ func ExtractImgURL(text string) []string {
 | 
			
		||||
	}
 | 
			
		||||
	return urls
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ExtractFileURLs(text string) []string {
 | 
			
		||||
	re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:docx?|pdf|pptx?|xlsx?|txt))`)
 | 
			
		||||
	matches := re.FindAllStringSubmatch(text, 10)
 | 
			
		||||
	urls := make([]string, 0)
 | 
			
		||||
	if len(matches) > 0 {
 | 
			
		||||
		for _, m := range matches {
 | 
			
		||||
			urls = append(urls, m[1])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return urls
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  presets: [
 | 
			
		||||
    '@vue/cli-plugin-babel/preset'
 | 
			
		||||
    '@vue.css/cli-plugin-babel/preset'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										237
									
								
								web/src/assets/css/markdown/vue.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								web/src/assets/css/markdown/vue.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
			
		||||
.chat-line {
 | 
			
		||||
    ol, ul {
 | 
			
		||||
        margin: 0.8em 0;
 | 
			
		||||
        list-style: normal;
 | 
			
		||||
    }
 | 
			
		||||
    a {
 | 
			
		||||
        color: #42b983;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        padding: 0 2px;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1,
 | 
			
		||||
    h2,
 | 
			
		||||
    h3,
 | 
			
		||||
    h4,
 | 
			
		||||
    h5,
 | 
			
		||||
    h6 {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        margin-top: 1rem;
 | 
			
		||||
        margin-bottom: 1rem;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        line-height: 1.4;
 | 
			
		||||
        cursor: text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1:hover a.anchor,
 | 
			
		||||
    h2:hover a.anchor,
 | 
			
		||||
    h3:hover a.anchor,
 | 
			
		||||
    h4:hover a.anchor,
 | 
			
		||||
    h5:hover a.anchor,
 | 
			
		||||
    h6:hover a.anchor {
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 tt,
 | 
			
		||||
    h1 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 tt,
 | 
			
		||||
    h2 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h3 tt,
 | 
			
		||||
    h3 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h4 tt,
 | 
			
		||||
    h4 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h5 tt,
 | 
			
		||||
    h5 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h6 tt,
 | 
			
		||||
    h6 code {
 | 
			
		||||
        font-size: inherit !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 a,
 | 
			
		||||
    h3 a {
 | 
			
		||||
        color: #34495e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 {
 | 
			
		||||
        padding-bottom: .4rem;
 | 
			
		||||
        font-size: 2.2rem;
 | 
			
		||||
        line-height: 1.3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h2 {
 | 
			
		||||
        font-size: 1.75rem;
 | 
			
		||||
        line-height: 1.225;
 | 
			
		||||
        margin: 35px 0 15px;
 | 
			
		||||
        padding-bottom: 0.5em;
 | 
			
		||||
        border-bottom: 1px solid #ddd;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h3 {
 | 
			
		||||
        font-size: 1.4rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        margin: 20px 0 7px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h4 {
 | 
			
		||||
        font-size: 1.2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h5 {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h6 {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        color: #777;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p,
 | 
			
		||||
    blockquote,
 | 
			
		||||
    ul,
 | 
			
		||||
    ol,
 | 
			
		||||
    dl,
 | 
			
		||||
    table {
 | 
			
		||||
        margin: 0.8em 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li > ol,
 | 
			
		||||
    li > ul {
 | 
			
		||||
        margin: 0 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hr {
 | 
			
		||||
        height: 2px;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        margin: 16px 0;
 | 
			
		||||
        background-color: #e7e7e7;
 | 
			
		||||
        border: 0 none;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        box-sizing: content-box;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h2:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h1:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h1:first-child + h2 {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body > h3:first-child,
 | 
			
		||||
    body > h4:first-child,
 | 
			
		||||
    body > h5:first-child,
 | 
			
		||||
    body > h6:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a:first-child h1,
 | 
			
		||||
    a:first-child h2,
 | 
			
		||||
    a:first-child h3,
 | 
			
		||||
    a:first-child h4,
 | 
			
		||||
    a:first-child h5,
 | 
			
		||||
    a:first-child h6 {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1 p,
 | 
			
		||||
    h2 p,
 | 
			
		||||
    h3 p,
 | 
			
		||||
    h4 p,
 | 
			
		||||
    h5 p,
 | 
			
		||||
    h6 p {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li p.first {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul,
 | 
			
		||||
    ol {
 | 
			
		||||
        padding-left: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul:first-child,
 | 
			
		||||
    ol:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul:last-child,
 | 
			
		||||
    ol:last-child {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    blockquote {
 | 
			
		||||
        border-left: 4px solid #42b983;
 | 
			
		||||
        padding: 10px 15px;
 | 
			
		||||
        color: #777;
 | 
			
		||||
        background-color: rgba(66, 185, 131, .1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table {
 | 
			
		||||
        padding: 0;
 | 
			
		||||
        word-break: initial;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr {
 | 
			
		||||
        border-top: 1px solid #dfe2e5;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr:nth-child(2n),
 | 
			
		||||
    thead {
 | 
			
		||||
        background-color: #fafafa;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        border: 1px solid #dfe2e5;
 | 
			
		||||
        border-bottom: 0;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 6px 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr td {
 | 
			
		||||
        border: 1px solid #dfe2e5;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        padding: 6px 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th:first-child,
 | 
			
		||||
    table tr td:first-child {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table tr th:last-child,
 | 
			
		||||
    table tr td:last-child {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,9 @@
 | 
			
		||||
                <el-image :src="GetFileIcon(file.ext)" fit="cover"  />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="body">
 | 
			
		||||
                <div class="title">{{file.name}}</div>
 | 
			
		||||
                <div class="title">
 | 
			
		||||
                  <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="info">
 | 
			
		||||
                  <span>{{GetFileType(file.ext)}}</span>
 | 
			
		||||
                  <span>{{FormatFileSize(file.size)}}</span>
 | 
			
		||||
@@ -121,6 +123,7 @@ onMounted(() => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
@import '@/assets/css/markdown/vue.css';
 | 
			
		||||
.chat-line-prompt {
 | 
			
		||||
  background-color #ffffff;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
@@ -214,11 +217,6 @@ onMounted(() => {
 | 
			
		||||
          margin 10px 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        a {
 | 
			
		||||
          color #20a0ff
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        p {
 | 
			
		||||
          line-height 1.5
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,7 @@ const reGenerate = (prompt) => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
@import '@/assets/css/markdown/vue.css';
 | 
			
		||||
.common-layout {
 | 
			
		||||
  .chat-line-reply {
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
@@ -132,18 +133,12 @@ const reGenerate = (prompt) => {
 | 
			
		||||
        .content {
 | 
			
		||||
          min-height 20px;
 | 
			
		||||
          word-break break-word;
 | 
			
		||||
          padding: 6px 10px;
 | 
			
		||||
          padding: 0 10px;
 | 
			
		||||
          color #374151;
 | 
			
		||||
          font-size: var(--content-font-size);
 | 
			
		||||
          border-radius: 5px;
 | 
			
		||||
          overflow auto;
 | 
			
		||||
 | 
			
		||||
          a {
 | 
			
		||||
            color #20a0ff
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // control the image size in content
 | 
			
		||||
 | 
			
		||||
          img {
 | 
			
		||||
            max-width: 600px;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
@@ -170,10 +165,11 @@ const reGenerate = (prompt) => {
 | 
			
		||||
 | 
			
		||||
          .code-container {
 | 
			
		||||
            position relative
 | 
			
		||||
            display flex
 | 
			
		||||
 | 
			
		||||
            .hljs {
 | 
			
		||||
              border-radius 10px
 | 
			
		||||
              line-height 1.5
 | 
			
		||||
              width 100%
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .copy-code-btn {
 | 
			
		||||
@@ -194,7 +190,7 @@ const reGenerate = (prompt) => {
 | 
			
		||||
          .lang-name {
 | 
			
		||||
            position absolute;
 | 
			
		||||
            right 10px
 | 
			
		||||
            bottom 50px
 | 
			
		||||
            bottom 20px
 | 
			
		||||
            padding 2px 6px 4px 6px
 | 
			
		||||
            background-color #444444
 | 
			
		||||
            border-radius 10px
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@
 | 
			
		||||
                  :auto-upload="true"
 | 
			
		||||
                  :show-file-list="false"
 | 
			
		||||
                  :http-request="afterRead"
 | 
			
		||||
                  accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf"
 | 
			
		||||
              >
 | 
			
		||||
                <el-icon class="avatar-uploader-icon">
 | 
			
		||||
                  <Plus/>
 | 
			
		||||
 
 | 
			
		||||
@@ -188,39 +188,8 @@ export function processContent(content) {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lines = content.split("\n")
 | 
			
		||||
    if (lines.length <= 1) {
 | 
			
		||||
    return content
 | 
			
		||||
}
 | 
			
		||||
    const texts = []
 | 
			
		||||
    // 定义匹配数学公式的正则表达式
 | 
			
		||||
    const formulaRegex = /^\s*[a-z|A-Z]+[^=]+\s*=\s*[^=]+$/;
 | 
			
		||||
    let hasCode = false
 | 
			
		||||
    for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
        // 处理引用块换行
 | 
			
		||||
        if (lines[i].startsWith(">")) {
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            texts.push("\n")
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        // 如果包含代码块则跳过公式检测
 | 
			
		||||
        if (lines[i].indexOf("```") !== -1) {
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            hasCode = true
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        // 识别并处理数学公式,需要排除那些已经被识别出来的公式
 | 
			
		||||
        if (i > 0 && formulaRegex.test(lines[i]) && lines[i - 1].indexOf("$$") === -1 && !hasCode) {
 | 
			
		||||
            texts.push("$$")
 | 
			
		||||
            texts.push(lines[i])
 | 
			
		||||
            texts.push("$$")
 | 
			
		||||
            continue
 | 
			
		||||
        }
 | 
			
		||||
        texts.push(lines[i])
 | 
			
		||||
    }
 | 
			
		||||
    return texts.join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function processPrompt(prompt) {
 | 
			
		||||
    return prompt.replace(/&/g, "&")
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
          <div class="content" :style="{height: leftBoxHeight+'px'}">
 | 
			
		||||
            <el-row v-for="chat in chatList" :key="chat.chat_id">
 | 
			
		||||
              <div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
 | 
			
		||||
                   @click="changeChat(chat)">
 | 
			
		||||
                   @click="loadChat(chat)">
 | 
			
		||||
                <el-image :src="chat.icon" class="avatar"/>
 | 
			
		||||
                <span class="chat-title-input" v-if="chat.edit">
 | 
			
		||||
                  <el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
 | 
			
		||||
@@ -424,12 +424,8 @@ const newChat = () => {
 | 
			
		||||
  connect(null, roleId.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换会话
 | 
			
		||||
const changeChat = (chat) => {
 | 
			
		||||
  localStorage.setItem("chat_id", chat.chat_id)
 | 
			
		||||
  loadChat(chat)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换会话
 | 
			
		||||
const loadChat = function (chat) {
 | 
			
		||||
  if (!isLogin.value) {
 | 
			
		||||
    store.setShowLoginDialog(true)
 | 
			
		||||
@@ -753,7 +749,7 @@ const sendMessage = function () {
 | 
			
		||||
  // 如果携带了文件,则串上文件地址
 | 
			
		||||
  let content = prompt.value
 | 
			
		||||
  if (files.value.length > 0) {
 | 
			
		||||
    content = files.value.map(file => file.url).join(" ") + " " +  content
 | 
			
		||||
    content += files.value.map(file => file.url).join(" ")
 | 
			
		||||
  }
 | 
			
		||||
  // 追加消息
 | 
			
		||||
  chatData.value.push({
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,7 @@
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="绑定API-KEY:" prop="apikey">
 | 
			
		||||
          <el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
 | 
			
		||||
          <el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
 | 
			
		||||
            <el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
 | 
			
		||||
              {{ v.name }}
 | 
			
		||||
              <el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
 | 
			
		||||
@@ -229,7 +229,7 @@ const platforms = ref([])
 | 
			
		||||
 | 
			
		||||
// 获取 API KEY
 | 
			
		||||
const apiKeys = ref([])
 | 
			
		||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
 | 
			
		||||
httpGet('/api/admin/apikey/list?type=chat').then(res => {
 | 
			
		||||
  apiKeys.value = res.data
 | 
			
		||||
}).catch(e => {
 | 
			
		||||
  ElMessage.error("获取 API KEY 失败:" + e.message)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user