').addClass('acc-message-content');
@@ -551,7 +614,6 @@ function addMessage(role, content) {
const contentDiv = lastMsg.find('.acc-message-content');
- // Simple escaping for stream chunks
const escapedContent = content
.replace(/&/g, "&")
.replace(/]*)?>[\s\S]*?<\/thinking>/gi, '')
- .replace(/<\/thinking>/gi, '') // Remove residual closing tags
+ .replace(/<\/thinking>/gi, '')
.replace(/
]*)?>[\s\S]*?<\/tool_code>/gi, '')
.trim();
@@ -581,7 +642,6 @@ function addMessage(role, content) {
'read_character_card', 'update_character_card', 'edit_character_text',
'manage_first_message', 'use_tool'
];
- // Match tools with potential attributes and whitespace
const regex = new RegExp(`<(${tools.join('|')})(?:\\s+[^>]*)?>[\\s\\S]*?<\\/\\1>`, 'gi');
displayContent = displayContent.replace(regex, '').trim();
@@ -589,15 +649,13 @@ function addMessage(role, content) {
displayContent = "(正在执行操作...)";
}
- // If this is a final assistant message, remove ANY streaming message
if (role === 'assistant') {
stream.find('.acc-streaming').remove();
}
}
let formattedContent;
-
- // Check if content is pre-formatted HTML (e.g. tool requests)
+
if (displayContent.trim().startsWith('/g, ">");
- // 2. Code Blocks (```...```)
html = html.replace(/```([\s\S]*?)```/g, '$1
');
- // 3. Inline Code (`...`)
html = html.replace(/`([^`]+)`/g, '$1');
- // 4. Headers
html = html.replace(/^#### (.*$)/gm, '$1
');
html = html.replace(/^### (.*$)/gm, '$1
');
html = html.replace(/^## (.*$)/gm, '$1
');
html = html.replace(/^# (.*$)/gm, '$1
');
- // 5. Bold & Italic
html = html.replace(/\*\*(.*?)\*\*/g, '$1');
html = html.replace(/\*(.*?)\*/g, '$1');
- // 6. Horizontal Rules
html = html.replace(/^[\*\-]{3,}$/gm, '
');
- // 7. Lists
- // Replace * or - at start of line with a bullet point
html = html.replace(/^\s*[\-\*]\s+(.*$)/gm, '$1');
- // 8. Line breaks
- // We want to preserve line breaks, but block elements (h1-h6, pre, li) handle their own spacing.
- // We can replace newlines with
ONLY if they are not around block tags.
- // A simpler approach for chat: just replace all \n with
, and let CSS handle the extra spacing or remove it.
- // But content shouldn't have
.
- // Let's just replace \n with
globally, but we need to protect blocks.
- // Actually, the code block regex above already consumed the newlines inside it.
- // So we can just replace remaining \n.
html = html.replace(/\n/g, '
');
@@ -688,9 +730,6 @@ function renderEditor() {
const container = $('#acc-preview-container');
const tabsContainer = $('.acc-preview-tabs');
- // Don't fully empty if we want to preserve state, but for simplicity we re-render tabs
- // To preserve scroll position or focus, we might need more complex logic.
- // For now, re-rendering is acceptable as long as data is in openedFiles.
container.empty();
tabsContainer.empty();
@@ -700,7 +739,6 @@ function renderEditor() {
return;
}
- // Ensure activeFileId is valid
if (!activeFileId || !openedFiles.has(activeFileId)) {
activeFileId = openedFiles.keys().next().value;
}
@@ -713,13 +751,12 @@ function renderEditor() {
.attr('title', file.title)
.on('click', () => {
activeFileId = id;
- renderEditor(); // Re-render to switch tabs
+ renderEditor();
});
const icon = $('');
const titleSpan = $('').addClass('acc-tab-title').text(file.title);
- // Close button for tab
const closeBtn = $('')
.html('×')
.addClass('acc-tab-close')
@@ -774,7 +811,6 @@ function renderEditor() {
})
.on('input', function() {
file.content = $(this).val();
- // file.isDirty = true; // Could add dirty indicator
});
contentDiv.append(textarea);
@@ -796,17 +832,23 @@ async function saveFile(id) {
try {
let result;
if (meta.type === 'char') {
- result = await tools.update_character_card({
- chid: meta.chid,
- [meta.field]: file.content
- });
+ if (meta.field.startsWith('greeting_')) {
+ const index = parseInt(meta.field.split('_')[1]);
+
+ result = await tools.manage_first_message({
+ action: 'update',
+ chid: meta.chid,
+ index: index + 1,
+ message: file.content
+ });
+ } else {
+ result = await tools.update_character_card({
+ chid: meta.chid,
+ [meta.field]: file.content
+ });
+ }
} else if (meta.type === 'wi') {
- // For WI, we need to construct the entry object
- // We assume file.content is the 'content' field of the entry
- // We need other fields like keys, etc. stored in metadata or parsed?
- // If we only have content, we might lose keys if we just write content.
- // But wait, write_world_info_entry takes an array of entry objects.
- // If we only update content, we need the UID.
+
if (meta.uid !== undefined) {
result = await tools.write_world_info_entry({
@@ -814,8 +856,7 @@ async function saveFile(id) {
entries: [{ uid: meta.uid, content: file.content }]
});
} else {
- // New entry? We need keys.
- // If it's a raw JSON view, we can parse it.
+
try {
const entry = JSON.parse(file.content);
result = await tools.write_world_info_entry({
@@ -823,8 +864,7 @@ async function saveFile(id) {
entries: [entry]
});
} catch (e) {
- // If it's just text content, we can't save it as a new entry without keys.
- // Unless we assume it's an update to an existing entry we know about.
+
toastr.error('保存失败: 内容必须是有效的 JSON (针对新建条目) 或包含 UID');
return;
}
@@ -852,9 +892,15 @@ async function loadContextToEditor() {
if (chid && chid !== 'new') {
try {
const charData = await tools.read_character_card({ chid });
- const char = JSON.parse(charData);
- previousCharData = char; // Cache for selector
-
+ const response = JSON.parse(charData);
+
+ if (response.status !== 'success' || !response.data) {
+ console.error("Failed to read character:", response);
+ return;
+ }
+
+ const char = response.data;
+ previousCharData = char;
const charGroup = $('