fix(pico): separate thought and normal messages

This commit is contained in:
lc6464
2026-04-10 20:23:12 +08:00
parent 748ac58dd1
commit c8bac699fe
15 changed files with 300 additions and 24 deletions
@@ -1,5 +1,6 @@
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { IconBrain, IconCheck, IconCopy } from "@tabler/icons-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import ReactMarkdown from "react-markdown"
import rehypeRaw from "rehype-raw"
import rehypeSanitize from "rehype-sanitize"
@@ -7,16 +8,20 @@ import remarkGfm from "remark-gfm"
import { Button } from "@/components/ui/button"
import { formatMessageTime } from "@/hooks/use-pico-chat"
import { cn } from "@/lib/utils"
interface AssistantMessageProps {
content: string
isThought?: boolean
timestamp?: string | number
}
export function AssistantMessage({
content,
isThought = false,
timestamp = "",
}: AssistantMessageProps) {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState(false)
const formattedTimestamp =
timestamp !== "" ? formatMessageTime(timestamp) : ""
@@ -33,6 +38,12 @@ export function AssistantMessage({
<div className="text-muted-foreground flex items-center justify-between gap-2 px-1 text-xs opacity-70">
<div className="flex items-center gap-2">
<span>PicoClaw</span>
{isThought && (
<span className="inline-flex items-center gap-1 rounded-full border border-amber-300/80 bg-amber-100/80 px-2 py-0.5 text-[11px] font-medium text-amber-800">
<IconBrain className="size-3" />
<span>{t("chat.reasoningLabel")}</span>
</span>
)}
{formattedTimestamp && (
<>
<span className="opacity-50"></span>
@@ -42,8 +53,22 @@ export function AssistantMessage({
</div>
</div>
<div className="bg-card text-card-foreground relative overflow-hidden rounded-xl border">
<div className="prose dark:prose-invert prose-p:my-2 prose-pre:my-2 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none p-4 text-[15px] leading-relaxed [overflow-wrap:anywhere] break-words">
<div
className={cn(
"relative overflow-hidden rounded-xl border",
isThought
? "border-amber-200/90 bg-amber-50/70 text-amber-950"
: "bg-card text-card-foreground",
)}
>
<div
className={cn(
"prose dark:prose-invert prose-pre:my-2 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none [overflow-wrap:anywhere] break-words",
isThought
? "prose-p:my-1.5 p-3 text-[13px] leading-relaxed opacity-90"
: "prose-p:my-2 p-4 text-[15px] leading-relaxed",
)}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
@@ -54,7 +79,12 @@ export function AssistantMessage({
<Button
variant="ghost"
size="icon"
className="bg-background/50 hover:bg-background/80 absolute top-2 right-2 h-7 w-7 opacity-0 transition-opacity group-hover:opacity-100"
className={cn(
"absolute top-2 right-2 h-7 w-7 opacity-0 transition-opacity group-hover:opacity-100",
isThought
? "bg-amber-100/70 hover:bg-amber-200/80"
: "bg-background/50 hover:bg-background/80",
)}
onClick={handleCopy}
>
{isCopied ? (
@@ -247,6 +247,7 @@ export function ChatPage() {
{msg.role === "assistant" ? (
<AssistantMessage
content={msg.content}
isThought={msg.kind === "thought"}
timestamp={msg.timestamp}
/>
) : (
+2 -1
View File
@@ -24,6 +24,7 @@ export async function loadSessionMessages(
id: `hist-${index}-${Date.now()}`,
role: message.role,
content: message.content,
kind: message.role === "assistant" ? "normal" : undefined,
attachments: toChatAttachments(message.media),
timestamp: fallbackTime,
}))
@@ -50,7 +51,7 @@ function messageSignature(message: ChatMessage): string {
return `${message.role}\u0000${message.content}\u0000${normalizeMessageTimestamp(
message.timestamp,
)}\u0000${attachmentSignature}`
)}\u0000${message.kind ?? ""}\u0000${attachmentSignature}`
}
function comparableTimestamp(timestamp: number | string): number {
+25 -2
View File
@@ -1,7 +1,10 @@
import { toast } from "sonner"
import { normalizeUnixTimestamp } from "@/features/chat/state"
import { updateChatStore } from "@/store/chat"
import {
type AssistantMessageKind,
updateChatStore,
} from "@/store/chat"
export interface PicoMessage {
type: string
@@ -11,6 +14,16 @@ export interface PicoMessage {
payload?: Record<string, unknown>
}
function parseAssistantMessageKind(
payload: Record<string, unknown>,
): AssistantMessageKind {
return payload.thought === true ? "thought" : "normal"
}
function hasAssistantKindPayload(payload: Record<string, unknown>): boolean {
return typeof payload.thought === "boolean"
}
export function handlePicoMessage(
message: PicoMessage,
expectedSessionId: string,
@@ -25,6 +38,7 @@ export function handlePicoMessage(
case "message.create": {
const content = (payload.content as string) || ""
const messageId = (payload.message_id as string) || `pico-${Date.now()}`
const kind = parseAssistantMessageKind(payload)
const timestamp =
message.timestamp !== undefined &&
Number.isFinite(Number(message.timestamp))
@@ -38,6 +52,7 @@ export function handlePicoMessage(
id: messageId,
role: "assistant",
content,
kind,
timestamp,
},
],
@@ -49,13 +64,21 @@ export function handlePicoMessage(
case "message.update": {
const content = (payload.content as string) || ""
const messageId = payload.message_id as string
const hasKind = hasAssistantKindPayload(payload)
const kind = parseAssistantMessageKind(payload)
if (!messageId) {
break
}
updateChatStore((prev) => ({
messages: prev.messages.map((msg) =>
msg.id === messageId ? { ...msg, content } : msg,
msg.id === messageId
? {
...msg,
content,
...(hasKind ? { kind } : {}),
}
: msg,
),
}))
break
+1
View File
@@ -47,6 +47,7 @@
"step3": "Preparing response...",
"step4": "Almost there..."
},
"reasoningLabel": "Reasoning",
"history": "History",
"noHistory": "No chat history yet",
"historyLoadFailed": "Failed to load chat history",
+1
View File
@@ -47,6 +47,7 @@
"step3": "准备回复...",
"step4": "马上就好..."
},
"reasoningLabel": "思考",
"history": "历史记录",
"noHistory": "暂无对话历史",
"historyLoadFailed": "加载历史记录失败",
+3
View File
@@ -11,11 +11,14 @@ export interface ChatAttachment {
filename?: string
}
export type AssistantMessageKind = "normal" | "thought"
export interface ChatMessage {
id: string
role: "user" | "assistant"
content: string
timestamp: number | string
kind?: AssistantMessageKind
attachments?: ChatAttachment[]
}