mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-26 23:11:34 +08:00
feat(llm): use ckeditor for text input area for mention support instead of textinput
This commit is contained in:
parent
3fae664877
commit
2c48a70bfb
@ -5,6 +5,7 @@ import BasicWidget from "../basic_widget.js";
|
|||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
|
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||||
|
|
||||||
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
||||||
import { formatMarkdown } from "./utils.js";
|
import { formatMarkdown } from "./utils.js";
|
||||||
@ -13,13 +14,16 @@ import { extractInChatToolSteps } from "./message_processor.js";
|
|||||||
import { validateEmbeddingProviders } from "./validation.js";
|
import { validateEmbeddingProviders } from "./validation.js";
|
||||||
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
|
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
|
||||||
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
|
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
|
||||||
|
import { ClassicEditor, type CKTextEditor, type MentionFeed } from "@triliumnext/ckeditor5";
|
||||||
|
import type { Suggestion } from "../../services/note_autocomplete.js";
|
||||||
|
|
||||||
import "../../stylesheets/llm_chat.css";
|
import "../../stylesheets/llm_chat.css";
|
||||||
|
|
||||||
export default class LlmChatPanel extends BasicWidget {
|
export default class LlmChatPanel extends BasicWidget {
|
||||||
private noteContextChatMessages!: HTMLElement;
|
private noteContextChatMessages!: HTMLElement;
|
||||||
private noteContextChatForm!: HTMLFormElement;
|
private noteContextChatForm!: HTMLFormElement;
|
||||||
private noteContextChatInput!: HTMLTextAreaElement;
|
private noteContextChatInput!: HTMLElement;
|
||||||
|
private noteContextChatInputEditor!: CKTextEditor;
|
||||||
private noteContextChatSendButton!: HTMLButtonElement;
|
private noteContextChatSendButton!: HTMLButtonElement;
|
||||||
private chatContainer!: HTMLElement;
|
private chatContainer!: HTMLElement;
|
||||||
private loadingIndicator!: HTMLElement;
|
private loadingIndicator!: HTMLElement;
|
||||||
@ -104,7 +108,7 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
const element = this.$widget[0];
|
const element = this.$widget[0];
|
||||||
this.noteContextChatMessages = element.querySelector('.note-context-chat-messages') as HTMLElement;
|
this.noteContextChatMessages = element.querySelector('.note-context-chat-messages') as HTMLElement;
|
||||||
this.noteContextChatForm = element.querySelector('.note-context-chat-form') as HTMLFormElement;
|
this.noteContextChatForm = element.querySelector('.note-context-chat-form') as HTMLFormElement;
|
||||||
this.noteContextChatInput = element.querySelector('.note-context-chat-input') as HTMLTextAreaElement;
|
this.noteContextChatInput = element.querySelector('.note-context-chat-input') as HTMLElement;
|
||||||
this.noteContextChatSendButton = element.querySelector('.note-context-chat-send-button') as HTMLButtonElement;
|
this.noteContextChatSendButton = element.querySelector('.note-context-chat-send-button') as HTMLButtonElement;
|
||||||
this.chatContainer = element.querySelector('.note-context-chat-container') as HTMLElement;
|
this.chatContainer = element.querySelector('.note-context-chat-container') as HTMLElement;
|
||||||
this.loadingIndicator = element.querySelector('.loading-indicator') as HTMLElement;
|
this.loadingIndicator = element.querySelector('.loading-indicator') as HTMLElement;
|
||||||
@ -124,15 +128,81 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initializeEventListeners();
|
// Initialize CKEditor with mention support (async)
|
||||||
|
this.initializeCKEditor().then(() => {
|
||||||
|
this.initializeEventListeners();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to initialize CKEditor, falling back to basic event listeners:', error);
|
||||||
|
this.initializeBasicEventListeners();
|
||||||
|
});
|
||||||
|
|
||||||
return this.$widget;
|
return this.$widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initializeCKEditor() {
|
||||||
|
const mentionSetup: MentionFeed[] = [
|
||||||
|
{
|
||||||
|
marker: "@",
|
||||||
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||||
|
itemRenderer: (item) => {
|
||||||
|
const suggestion = item as Suggestion;
|
||||||
|
const itemElement = document.createElement("button");
|
||||||
|
itemElement.innerHTML = `${suggestion.highlightedNotePathTitle} `;
|
||||||
|
return itemElement;
|
||||||
|
},
|
||||||
|
minimumCharacters: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.noteContextChatInputEditor = await ClassicEditor.create(this.noteContextChatInput, {
|
||||||
|
toolbar: {
|
||||||
|
items: [] // No toolbar for chat input
|
||||||
|
},
|
||||||
|
placeholder: this.noteContextChatInput.getAttribute('data-placeholder') || 'Enter your message...',
|
||||||
|
mention: {
|
||||||
|
feeds: mentionSetup
|
||||||
|
},
|
||||||
|
licenseKey: "GPL"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set minimal height
|
||||||
|
const editorElement = this.noteContextChatInputEditor.ui.getEditableElement();
|
||||||
|
if (editorElement) {
|
||||||
|
editorElement.style.minHeight = '60px';
|
||||||
|
editorElement.style.maxHeight = '200px';
|
||||||
|
editorElement.style.overflowY = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up keybindings after editor is ready
|
||||||
|
this.setupEditorKeyBindings();
|
||||||
|
|
||||||
|
console.log('CKEditor initialized successfully for LLM chat input');
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeBasicEventListeners() {
|
||||||
|
// Fallback event listeners for when CKEditor fails to initialize
|
||||||
|
this.noteContextChatForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// In fallback mode, the noteContextChatInput should contain a textarea
|
||||||
|
const textarea = this.noteContextChatInput.querySelector('textarea');
|
||||||
|
if (textarea) {
|
||||||
|
const content = textarea.value;
|
||||||
|
this.sendMessage(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
console.log(`LlmChatPanel cleanup called, removing any active WebSocket subscriptions`);
|
console.log(`LlmChatPanel cleanup called, removing any active WebSocket subscriptions`);
|
||||||
this._messageHandler = null;
|
this._messageHandler = null;
|
||||||
this._messageHandlerId = null;
|
this._messageHandlerId = null;
|
||||||
|
|
||||||
|
// Clean up CKEditor instance
|
||||||
|
if (this.noteContextChatInputEditor) {
|
||||||
|
this.noteContextChatInputEditor.destroy().catch(error => {
|
||||||
|
console.error('Error destroying CKEditor:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -531,18 +601,31 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
private async sendMessage(content: string) {
|
private async sendMessage(content: string) {
|
||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
// Extract mentions from the content if using CKEditor
|
||||||
|
let mentions: Array<{noteId: string; title: string; notePath: string}> = [];
|
||||||
|
let plainTextContent = content;
|
||||||
|
|
||||||
|
if (this.noteContextChatInputEditor) {
|
||||||
|
const extracted = this.extractMentionsAndContent(content);
|
||||||
|
mentions = extracted.mentions;
|
||||||
|
plainTextContent = extracted.content;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the user message to the UI and data model
|
// Add the user message to the UI and data model
|
||||||
this.addMessageToChat('user', content);
|
this.addMessageToChat('user', plainTextContent);
|
||||||
this.messages.push({
|
this.messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: content
|
content: plainTextContent,
|
||||||
|
mentions: mentions.length > 0 ? mentions : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save the data immediately after a user message
|
// Save the data immediately after a user message
|
||||||
await this.saveCurrentData();
|
await this.saveCurrentData();
|
||||||
|
|
||||||
// Clear input and show loading state
|
// Clear input and show loading state
|
||||||
this.noteContextChatInput.value = '';
|
if (this.noteContextChatInputEditor) {
|
||||||
|
this.noteContextChatInputEditor.setData('');
|
||||||
|
}
|
||||||
showLoadingIndicator(this.loadingIndicator);
|
showLoadingIndicator(this.loadingIndicator);
|
||||||
this.hideSources();
|
this.hideSources();
|
||||||
|
|
||||||
@ -555,9 +638,10 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
|
|
||||||
// Create the message parameters
|
// Create the message parameters
|
||||||
const messageParams = {
|
const messageParams = {
|
||||||
content,
|
content: plainTextContent,
|
||||||
useAdvancedContext,
|
useAdvancedContext,
|
||||||
showThinking
|
showThinking,
|
||||||
|
mentions: mentions.length > 0 ? mentions : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try websocket streaming (preferred method)
|
// Try websocket streaming (preferred method)
|
||||||
@ -621,7 +705,9 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear input and show loading state
|
// Clear input and show loading state
|
||||||
this.noteContextChatInput.value = '';
|
if (this.noteContextChatInputEditor) {
|
||||||
|
this.noteContextChatInputEditor.setData('');
|
||||||
|
}
|
||||||
showLoadingIndicator(this.loadingIndicator);
|
showLoadingIndicator(this.loadingIndicator);
|
||||||
this.hideSources();
|
this.hideSources();
|
||||||
|
|
||||||
@ -1213,22 +1299,122 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
private initializeEventListeners() {
|
private initializeEventListeners() {
|
||||||
this.noteContextChatForm.addEventListener('submit', (e) => {
|
this.noteContextChatForm.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = this.noteContextChatInput.value;
|
|
||||||
this.sendMessage(content);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add auto-resize functionality to the textarea
|
let content = '';
|
||||||
this.noteContextChatInput.addEventListener('input', () => {
|
|
||||||
this.noteContextChatInput.style.height = 'auto';
|
|
||||||
this.noteContextChatInput.style.height = `${this.noteContextChatInput.scrollHeight}px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle Enter key (send on Enter, new line on Shift+Enter)
|
if (this.noteContextChatInputEditor && this.noteContextChatInputEditor.getData) {
|
||||||
this.noteContextChatInput.addEventListener('keydown', (e) => {
|
// Use CKEditor content
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
content = this.noteContextChatInputEditor.getData();
|
||||||
e.preventDefault();
|
} else {
|
||||||
this.noteContextChatForm.dispatchEvent(new Event('submit'));
|
// Fallback: check if there's a textarea (fallback mode)
|
||||||
|
const textarea = this.noteContextChatInput.querySelector('textarea');
|
||||||
|
if (textarea) {
|
||||||
|
content = textarea.value;
|
||||||
|
} else {
|
||||||
|
// Last resort: try to get text content from the div
|
||||||
|
content = this.noteContextChatInput.textContent || this.noteContextChatInput.innerText || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.trim()) {
|
||||||
|
this.sendMessage(content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Enter key (send on Enter, new line on Shift+Enter) via CKEditor
|
||||||
|
// We'll set this up after CKEditor is initialized
|
||||||
|
this.setupEditorKeyBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEditorKeyBindings() {
|
||||||
|
if (this.noteContextChatInputEditor && this.noteContextChatInputEditor.keystrokes) {
|
||||||
|
try {
|
||||||
|
this.noteContextChatInputEditor.keystrokes.set('Enter', (key, stop) => {
|
||||||
|
if (!key.shiftKey) {
|
||||||
|
stop();
|
||||||
|
this.noteContextChatForm.dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('CKEditor keybindings set up successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to set up CKEditor keybindings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract note mentions and content from CKEditor
|
||||||
|
*/
|
||||||
|
private extractMentionsAndContent(editorData: string): { content: string; mentions: Array<{noteId: string; title: string; notePath: string}> } {
|
||||||
|
const mentions: Array<{noteId: string; title: string; notePath: string}> = [];
|
||||||
|
|
||||||
|
// Parse the HTML content to extract mentions
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = editorData;
|
||||||
|
|
||||||
|
// Find all mention elements - CKEditor uses specific patterns for mentions
|
||||||
|
// Look for elements with data-mention attribute or specific mention classes
|
||||||
|
const mentionElements = tempDiv.querySelectorAll('[data-mention], .mention, span[data-id]');
|
||||||
|
|
||||||
|
mentionElements.forEach(mentionEl => {
|
||||||
|
try {
|
||||||
|
// Try different ways to extract mention data based on CKEditor's format
|
||||||
|
let mentionData: any = null;
|
||||||
|
|
||||||
|
// Method 1: data-mention attribute (JSON format)
|
||||||
|
if (mentionEl.hasAttribute('data-mention')) {
|
||||||
|
mentionData = JSON.parse(mentionEl.getAttribute('data-mention') || '{}');
|
||||||
|
}
|
||||||
|
// Method 2: data-id attribute (simple format)
|
||||||
|
else if (mentionEl.hasAttribute('data-id')) {
|
||||||
|
const dataId = mentionEl.getAttribute('data-id');
|
||||||
|
const textContent = mentionEl.textContent || '';
|
||||||
|
|
||||||
|
// Parse the dataId to extract note information
|
||||||
|
if (dataId && dataId.startsWith('@')) {
|
||||||
|
const cleanId = dataId.substring(1); // Remove the @
|
||||||
|
mentionData = {
|
||||||
|
id: cleanId,
|
||||||
|
name: textContent,
|
||||||
|
notePath: cleanId // Assume the ID contains the path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Method 3: Check if this is a reference link (href=#notePath)
|
||||||
|
else if (mentionEl.tagName === 'A' && mentionEl.hasAttribute('href')) {
|
||||||
|
const href = mentionEl.getAttribute('href');
|
||||||
|
if (href && href.startsWith('#')) {
|
||||||
|
const notePath = href.substring(1);
|
||||||
|
mentionData = {
|
||||||
|
notePath: notePath,
|
||||||
|
noteTitle: mentionEl.textContent || 'Unknown Note'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentionData && (mentionData.notePath || mentionData.link)) {
|
||||||
|
const notePath = mentionData.notePath || mentionData.link?.substring(1); // Remove # from link
|
||||||
|
const noteId = notePath ? notePath.split('/').pop() : null;
|
||||||
|
const title = mentionData.noteTitle || mentionData.name || mentionEl.textContent || 'Unknown Note';
|
||||||
|
|
||||||
|
if (noteId) {
|
||||||
|
mentions.push({
|
||||||
|
noteId: noteId,
|
||||||
|
title: title,
|
||||||
|
notePath: notePath
|
||||||
|
});
|
||||||
|
console.log(`Extracted mention: noteId=${noteId}, title=${title}, notePath=${notePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse mention data:', e, mentionEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to plain text for the LLM, but preserve the structure
|
||||||
|
const content = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
|
||||||
|
console.log(`Extracted ${mentions.length} mentions from editor content`);
|
||||||
|
return { content, mentions };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,11 @@ export interface MessageData {
|
|||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
|
mentions?: Array<{
|
||||||
|
noteId: string;
|
||||||
|
title: string;
|
||||||
|
notePath: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatData {
|
export interface ChatData {
|
||||||
|
@ -31,11 +31,11 @@ export const TPL = `
|
|||||||
|
|
||||||
<form class="note-context-chat-form d-flex flex-column border-top p-2">
|
<form class="note-context-chat-form d-flex flex-column border-top p-2">
|
||||||
<div class="d-flex chat-input-container mb-2">
|
<div class="d-flex chat-input-container mb-2">
|
||||||
<textarea
|
<div
|
||||||
class="form-control note-context-chat-input"
|
class="form-control note-context-chat-input flex-grow-1"
|
||||||
placeholder="${t('ai_llm.enter_message')}"
|
style="min-height: 60px; max-height: 200px; overflow-y: auto;"
|
||||||
rows="2"
|
data-placeholder="${t('ai_llm.enter_message')}"
|
||||||
></textarea>
|
></div>
|
||||||
<button type="submit" class="btn btn-primary note-context-chat-send-button ms-2 d-flex align-items-center justify-content-center">
|
<button type="submit" class="btn btn-primary note-context-chat-send-button ms-2 d-flex align-items-center justify-content-center">
|
||||||
<i class="bx bx-send"></i>
|
<i class="bx bx-send"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -808,7 +808,7 @@ async function streamMessage(req: Request, res: Response) {
|
|||||||
log.info("=== Starting streamMessage ===");
|
log.info("=== Starting streamMessage ===");
|
||||||
try {
|
try {
|
||||||
const chatNoteId = req.params.chatNoteId;
|
const chatNoteId = req.params.chatNoteId;
|
||||||
const { content, useAdvancedContext, showThinking } = req.body;
|
const { content, useAdvancedContext, showThinking, mentions } = req.body;
|
||||||
|
|
||||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||||
throw new Error('Content cannot be empty');
|
throw new Error('Content cannot be empty');
|
||||||
@ -823,17 +823,51 @@ async function streamMessage(req: Request, res: Response) {
|
|||||||
// Update last active timestamp
|
// Update last active timestamp
|
||||||
session.lastActive = new Date();
|
session.lastActive = new Date();
|
||||||
|
|
||||||
// Add user message to the session
|
// Process mentions if provided
|
||||||
|
let enhancedContent = content;
|
||||||
|
if (mentions && Array.isArray(mentions) && mentions.length > 0) {
|
||||||
|
log.info(`Processing ${mentions.length} note mentions`);
|
||||||
|
|
||||||
|
// Import note service to get note content
|
||||||
|
const becca = (await import('../../becca/becca.js')).default;
|
||||||
|
|
||||||
|
const mentionContexts: string[] = [];
|
||||||
|
|
||||||
|
for (const mention of mentions) {
|
||||||
|
try {
|
||||||
|
const note = becca.getNote(mention.noteId);
|
||||||
|
if (note && !note.isDeleted) {
|
||||||
|
const noteContent = note.getContent();
|
||||||
|
if (noteContent && typeof noteContent === 'string' && noteContent.trim()) {
|
||||||
|
mentionContexts.push(`\n\n--- Content from "${mention.title}" (${mention.noteId}) ---\n${noteContent}\n--- End of "${mention.title}" ---`);
|
||||||
|
log.info(`Added content from note "${mention.title}" (${mention.noteId})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info(`Referenced note not found or deleted: ${mention.noteId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error retrieving content for note ${mention.noteId}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance the content with note references
|
||||||
|
if (mentionContexts.length > 0) {
|
||||||
|
enhancedContent = `${content}\n\n=== Referenced Notes ===\n${mentionContexts.join('\n')}`;
|
||||||
|
log.info(`Enhanced content with ${mentionContexts.length} note references`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user message to the session (with enhanced content for processing)
|
||||||
session.messages.push({
|
session.messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
content: enhancedContent,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create request parameters for the pipeline
|
// Create request parameters for the pipeline
|
||||||
const requestParams = {
|
const requestParams = {
|
||||||
chatNoteId: chatNoteId,
|
chatNoteId: chatNoteId,
|
||||||
content,
|
content: enhancedContent,
|
||||||
useAdvancedContext: useAdvancedContext === true,
|
useAdvancedContext: useAdvancedContext === true,
|
||||||
showThinking: showThinking === true,
|
showThinking: showThinking === true,
|
||||||
stream: true // Always stream for this endpoint
|
stream: true // Always stream for this endpoint
|
||||||
@ -851,9 +885,9 @@ async function streamMessage(req: Request, res: Response) {
|
|||||||
params: {
|
params: {
|
||||||
chatNoteId: chatNoteId
|
chatNoteId: chatNoteId
|
||||||
},
|
},
|
||||||
// Make sure the original content is available to the handler
|
// Make sure the enhanced content is available to the handler
|
||||||
body: {
|
body: {
|
||||||
content,
|
content: enhancedContent,
|
||||||
useAdvancedContext: useAdvancedContext === true,
|
useAdvancedContext: useAdvancedContext === true,
|
||||||
showThinking: showThinking === true
|
showThinking: showThinking === true
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user