mirror of
https://github.com/zadam/trilium.git
synced 2026-07-13 08:43:58 +02:00
feat(llm): display thinking process
This commit is contained in:
@@ -22,6 +22,7 @@ export interface Citation {
|
||||
|
||||
export interface StreamCallbacks {
|
||||
onChunk: (text: string) => void;
|
||||
onThinking?: (text: string) => void;
|
||||
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
|
||||
onToolResult?: (toolName: string, result: string) => void;
|
||||
onCitation?: (citation: Citation) => void;
|
||||
@@ -80,6 +81,9 @@ export async function streamChatCompletion(
|
||||
case "text":
|
||||
callbacks.onChunk(data.content);
|
||||
break;
|
||||
case "thinking":
|
||||
callbacks.onThinking?.(data.content);
|
||||
break;
|
||||
case "tool_use":
|
||||
callbacks.onToolUse?.(data.toolName, data.toolInput);
|
||||
break;
|
||||
|
||||
@@ -1619,7 +1619,9 @@
|
||||
"searching_web": "Searching the web...",
|
||||
"web_search": "Web search",
|
||||
"sources": "Sources",
|
||||
"extended_thinking": "Extended thinking"
|
||||
"extended_thinking": "Extended thinking",
|
||||
"thinking": "Thinking...",
|
||||
"thought_process": "Thought process"
|
||||
},
|
||||
"shared_switch": {
|
||||
"shared": "Shared",
|
||||
|
||||
@@ -10,7 +10,7 @@ marked.setOptions({
|
||||
gfm: true // GitHub Flavored Markdown
|
||||
});
|
||||
|
||||
type MessageType = "message" | "error";
|
||||
type MessageType = "message" | "error" | "thinking";
|
||||
|
||||
interface StoredMessage {
|
||||
id: string;
|
||||
@@ -30,21 +30,39 @@ interface Props {
|
||||
export default function ChatMessage({ message, isStreaming }: Props) {
|
||||
const roleLabel = message.role === "user" ? "You" : "Assistant";
|
||||
const isError = message.type === "error";
|
||||
const isThinking = message.type === "thinking";
|
||||
|
||||
// Only render markdown for assistant messages (not errors)
|
||||
// Render markdown for assistant messages (not errors or thinking)
|
||||
const renderedContent = useMemo(() => {
|
||||
if (message.role === "assistant" && !isError) {
|
||||
if (message.role === "assistant" && !isError && !isThinking) {
|
||||
return marked.parse(message.content) as string;
|
||||
}
|
||||
return null;
|
||||
}, [message.content, message.role, isError]);
|
||||
}, [message.content, message.role, isError, isThinking]);
|
||||
|
||||
const messageClasses = [
|
||||
"llm-chat-message",
|
||||
`llm-chat-message-${message.role}`,
|
||||
isError && "llm-chat-message-error"
|
||||
isError && "llm-chat-message-error",
|
||||
isThinking && "llm-chat-message-thinking"
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
// Render thinking messages in a collapsible details element
|
||||
if (isThinking) {
|
||||
return (
|
||||
<details className={messageClasses}>
|
||||
<summary className="llm-chat-thinking-summary">
|
||||
<span className="bx bx-brain" />
|
||||
{t("llm_chat.thought_process")}
|
||||
</summary>
|
||||
<div className="llm-chat-message-content llm-chat-thinking-content">
|
||||
{message.content}
|
||||
{isStreaming && <span className="llm-chat-cursor" />}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={messageClasses}>
|
||||
<div className="llm-chat-message-role">
|
||||
|
||||
@@ -164,6 +164,51 @@
|
||||
color: var(--danger-text-color, #c00);
|
||||
}
|
||||
|
||||
/* Thinking message (collapsible) */
|
||||
.llm-chat-message-thinking {
|
||||
background: var(--accented-background-color);
|
||||
border: 1px dashed var(--main-border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.llm-chat-thinking-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted-text-color);
|
||||
padding: 0.25rem 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.llm-chat-thinking-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.llm-chat-thinking-summary::before {
|
||||
content: "▶";
|
||||
font-size: 0.7em;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.llm-chat-message-thinking[open] .llm-chat-thinking-summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.llm-chat-thinking-summary .bx {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.llm-chat-thinking-content {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-text-color);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Input form */
|
||||
.llm-chat-input-form {
|
||||
display: flex;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TypeWidgetProps } from "../type_widget.js";
|
||||
import ChatMessage from "./ChatMessage.js";
|
||||
import "./LlmChat.css";
|
||||
|
||||
type MessageType = "message" | "error";
|
||||
type MessageType = "message" | "error" | "thinking";
|
||||
|
||||
interface StoredMessage {
|
||||
id: string;
|
||||
@@ -30,6 +30,7 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [streamingThinking, setStreamingThinking] = useState("");
|
||||
const [toolActivity, setToolActivity] = useState<string | null>(null);
|
||||
const [pendingCitations, setPendingCitations] = useState<Citation[]>([]);
|
||||
const [enableWebSearch, setEnableWebSearch] = useState(true);
|
||||
@@ -45,7 +46,7 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, streamingContent, toolActivity, scrollToBottom]);
|
||||
}, [messages, streamingContent, streamingThinking, toolActivity, scrollToBottom]);
|
||||
|
||||
// Use a ref to store the latest messages for getData
|
||||
const messagesRef = useRef(messages);
|
||||
@@ -120,8 +121,10 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
setInput("");
|
||||
setIsStreaming(true);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
|
||||
let assistantContent = "";
|
||||
let thinkingContent = "";
|
||||
const citations: Citation[] = [];
|
||||
|
||||
const apiMessages: ChatMessageData[] = newMessages.map(m => ({
|
||||
@@ -138,6 +141,11 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
setStreamingContent(assistantContent);
|
||||
setToolActivity(null); // Clear tool activity when text starts
|
||||
},
|
||||
onThinking: (text) => {
|
||||
thinkingContent += text;
|
||||
setStreamingThinking(thinkingContent);
|
||||
setToolActivity(t("llm_chat.thinking"));
|
||||
},
|
||||
onToolUse: (toolName, _input) => {
|
||||
const toolLabel = toolName === "web_search"
|
||||
? t("llm_chat.searching_web")
|
||||
@@ -160,22 +168,41 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setIsStreaming(false);
|
||||
setToolActivity(null);
|
||||
setShouldSave(true);
|
||||
},
|
||||
onDone: () => {
|
||||
const newMessages: StoredMessage[] = [];
|
||||
|
||||
// Save thinking as a separate message if present
|
||||
if (thinkingContent) {
|
||||
newMessages.push({
|
||||
id: crypto.randomUUID(),
|
||||
role: "assistant",
|
||||
content: thinkingContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: "thinking"
|
||||
});
|
||||
}
|
||||
|
||||
if (assistantContent) {
|
||||
const assistantMessage: StoredMessage = {
|
||||
newMessages.push({
|
||||
id: crypto.randomUUID(),
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
createdAt: new Date().toISOString(),
|
||||
citations: citations.length > 0 ? citations : undefined
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
});
|
||||
}
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
setMessages(prev => [...prev, ...newMessages]);
|
||||
}
|
||||
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setPendingCitations([]);
|
||||
setIsStreaming(false);
|
||||
setToolActivity(null);
|
||||
@@ -214,12 +241,24 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
{messages.map(msg => (
|
||||
<ChatMessage key={msg.id} message={msg} />
|
||||
))}
|
||||
{toolActivity && (
|
||||
{toolActivity && !streamingThinking && (
|
||||
<div className="llm-chat-tool-activity">
|
||||
<span className="llm-chat-tool-spinner" />
|
||||
{toolActivity}
|
||||
</div>
|
||||
)}
|
||||
{isStreaming && streamingThinking && (
|
||||
<ChatMessage
|
||||
message={{
|
||||
id: "streaming-thinking",
|
||||
role: "assistant",
|
||||
content: streamingThinking,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: "thinking"
|
||||
}}
|
||||
isStreaming
|
||||
/>
|
||||
)}
|
||||
{isStreaming && streamingContent && (
|
||||
<ChatMessage
|
||||
message={{
|
||||
|
||||
@@ -59,6 +59,7 @@ export class AnthropicProvider implements LlmProvider {
|
||||
type: "enabled",
|
||||
budget_tokens: thinkingBudget
|
||||
};
|
||||
console.log(`[LLM] Extended thinking enabled with budget: ${thinkingBudget} tokens`);
|
||||
}
|
||||
|
||||
const stream = this.client.messages.stream(streamParams);
|
||||
@@ -73,11 +74,15 @@ export class AnthropicProvider implements LlmProvider {
|
||||
toolName: block.name,
|
||||
toolInput: {} // Input comes in deltas
|
||||
};
|
||||
} else if (block.type === "thinking") {
|
||||
console.log("[LLM] Thinking block started");
|
||||
}
|
||||
} else if (event.type === "content_block_delta") {
|
||||
const delta = event.delta;
|
||||
if (delta.type === "text_delta") {
|
||||
yield { type: "text", content: delta.text };
|
||||
} else if (delta.type === "thinking_delta") {
|
||||
yield { type: "thinking", content: (delta as any).thinking };
|
||||
} else if (delta.type === "input_json_delta") {
|
||||
// Tool input is being streamed - we could accumulate it
|
||||
// For now, we already emitted tool_use at start
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface LlmMessage {
|
||||
*/
|
||||
export type LlmStreamChunk =
|
||||
| { type: "text"; content: string }
|
||||
| { type: "thinking"; content: string }
|
||||
| { type: "tool_use"; toolName: string; toolInput: Record<string, unknown> }
|
||||
| { type: "tool_result"; toolName: string; result: string }
|
||||
| { type: "citation"; url: string; title?: string }
|
||||
|
||||
Reference in New Issue
Block a user