diff --git a/adapter/deepseek/chat.go b/adapter/deepseek/chat.go index 332895d..3103f45 100644 --- a/adapter/deepseek/chat.go +++ b/adapter/deepseek/chat.go @@ -6,7 +6,6 @@ import ( "chat/utils" "errors" "fmt" - "strings" ) type ChatInstance struct { @@ -51,9 +50,18 @@ func (c *ChatInstance) GetChatEndpoint() string { } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { + messages := props.Message + // because of deepseek first message must be user role + // convert assistant message to user message + if len(messages) > 0 && messages[0].Role == globals.Assistant { + messages = make([]globals.Message, len(props.Message)) + copy(messages, props.Message) + messages[0].Role = globals.User + } + return ChatRequest{ Model: props.Model, - Messages: props.Message, + Messages: messages, MaxTokens: props.MaxTokens, Stream: stream, Temperature: props.Temperature, @@ -92,28 +100,23 @@ func (c *ChatInstance) ProcessLine(data string) (string, error) { delta := form.Choices[0].Delta - if delta.ReasoningContent != nil { - if *delta.ReasoningContent == "" && delta.Content != "" { - if !c.isReasonOver { - c.isReasonOver = true - - return fmt.Sprintf("\n\n%s", delta.Content), nil - } + if c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil { + c.isReasonOver = true + if delta.Content != "" { + return fmt.Sprintf("\n\n\n%s", delta.Content), nil } + return "\n\n\n", nil } - if delta.ReasoningContent != nil && delta.Content == "" { + if delta.ReasoningContent != nil { content := *delta.ReasoningContent - // replace double newlines with single newlines for markdown - if strings.Contains(content, "\n\n") { - content = strings.ReplaceAll(content, "\n\n", "\n") - } if c.isFirstReasoning { c.isFirstReasoning = false - return fmt.Sprintf(">%s", content), nil + return fmt.Sprintf("\n%s", content), nil } return content, nil } + return delta.Content, nil } @@ -150,7 +153,7 @@ func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string message := data.Choices[0].Message content := message.Content if message.ReasoningContent != nil { - content = fmt.Sprintf(">%s\n\n%s", *message.ReasoningContent, content) + content = fmt.Sprintf("\n%s\n\n\n%s", *message.ReasoningContent, content) } return content, nil diff --git a/adapter/deepseek/reflect.go b/adapter/deepseek/reflect.go deleted file mode 100644 index cc38a2b..0000000 --- a/adapter/deepseek/reflect.go +++ /dev/null @@ -1,9 +0,0 @@ -package deepseek - -import "reflect" - -var _ = reflect.TypeOf(ChatInstance{}) -var _ = reflect.TypeOf(ChatRequest{}) -var _ = reflect.TypeOf(ChatResponse{}) -var _ = reflect.TypeOf(ChatStreamResponse{}) -var _ = reflect.TypeOf(ChatStreamErrorResponse{}) diff --git a/app/src/components/Message.tsx b/app/src/components/Message.tsx index b777c64..0faf38e 100644 --- a/app/src/components/Message.tsx +++ b/app/src/components/Message.tsx @@ -19,7 +19,6 @@ import { copyClipboard, isContainDom, saveAsFile, - useInputValue, } from "@/utils/dom.ts"; import { useTranslation } from "react-i18next"; import React, { Ref, useRef, useState } from "react"; @@ -35,6 +34,7 @@ import Avatar from "@/components/Avatar.tsx"; import { useSelector } from "react-redux"; import { selectUsername } from "@/store/auth.ts"; import { appLogo } from "@/conf/env.ts"; +import { ThinkContent } from "@/components/ThinkContent"; type MessageProps = { index: number; @@ -180,7 +180,13 @@ function MessageMenu({ {t("message.copy")} useInputValue("input", filterMessage(message.content))} + onClick={() => { + const input = document.getElementById("input") as HTMLInputElement; + if (input) { + input.value = filterMessage(message.content); + input.focus(); + } + }} > {t("message.use")} @@ -223,12 +229,34 @@ function MessageContent({ username, }: MessageProps) { const isUser = message.role === "user"; - const user = useSelector(selectUsername); const [open, setOpen] = useState(false); const [editedMessage, setEditedMessage] = useState(""); + // 解析思考内容 + const parseThinkContent = (content: string) => { + if (message.role !== "assistant") return null; + + const startMatch = content.match(/\n?(.*?)(?:<\/think>|$)/s); + if (startMatch) { + const thinkContent = startMatch[1]; + const hasEndTag = content.includes(''); + const restContent = hasEndTag ? + content.replace(startMatch[0], "").trim() : + content.substring(content.indexOf('') + 7).trim(); + + return { + thinkContent, + restContent: hasEndTag ? restContent : '', + isComplete: hasEndTag + }; + } + return null; + }; + + const parsedContent = message.content.length ? parseThinkContent(message.content) : null; + return (
{message.content.length ? ( - + <> + {parsedContent ? ( + <> + + {parsedContent.restContent && ( + + )} + + ) : ( + + )} + ) : message.end === true ? ( ) : ( diff --git a/app/src/components/ThinkContent.tsx b/app/src/components/ThinkContent.tsx new file mode 100644 index 0000000..1cdd271 --- /dev/null +++ b/app/src/components/ThinkContent.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { ChevronDown, ChevronUp, Brain, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/components/ui/lib/utils"; +import Markdown from "@/components/Markdown"; +import { useTranslation } from "react-i18next"; + +interface ThinkContentProps { + content: string; + isComplete?: boolean; +} + +export function ThinkContent({ content, isComplete = true }: ThinkContentProps) { + const [isExpanded, setIsExpanded] = useState(true); + const { t } = useTranslation(); + + const toggleExpand = () => { + setIsExpanded(!isExpanded); + }; + + return ( +
+ + +
+
+ {content} +
+
+
+ ); +}