diff --git a/css/main.css b/css/main.css index 8ca5b33f..9dce4d0e 100644 --- a/css/main.css +++ b/css/main.css @@ -625,19 +625,19 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* { width: 100%; overflow-y: visible; } - + .message { break-inside: avoid; } - + .gradio-container { overflow: visible; } - + .tab-nav { display: none !important; } - + #chat-tab > :first-child { max-width: unset; } @@ -1308,3 +1308,77 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* { padding-left: 1rem; padding-right: 1rem; } + +/* Thinking blocks styling */ +.thinking-block { + margin-bottom: 12px; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.1); + background-color: var(--light-theme-gray); + overflow: hidden; +} + +.dark .thinking-block { + background-color: var(--darker-gray); +} + +.thinking-header { + display: flex; + align-items: center; + padding: 10px 16px; + cursor: pointer; + user-select: none; + font-size: 14px; + color: rgba(0, 0, 0, 0.7); + transition: background-color 0.2s; +} + +.thinking-header:hover { + background-color: rgba(0, 0, 0, 0.03); +} + +.thinking-header::-webkit-details-marker { + display: none; +} + +.thinking-icon { + margin-right: 8px; + color: rgba(0, 0, 0, 0.5); +} + +.thinking-title { + font-weight: 500; +} + +.thinking-content { + padding: 12px 16px; + border-top: 1px solid rgba(0, 0, 0, 0.07); + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 1.5; + overflow-wrap: break-word; + max-height: 300px; + overflow-y: scroll; + contain: layout; +} + +/* Animation for opening thinking blocks */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.thinking-block[open] .thinking-content { + animation: fadeIn 0.3s ease-out; +} + +/* Additional style for in-progress thinking */ +.thinking-block[data-streaming="true"] .thinking-title { + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } +} diff --git a/js/global_scope_js.js b/js/global_scope_js.js index f308edb9..e808c473 100644 --- a/js/global_scope_js.js +++ b/js/global_scope_js.js @@ -31,24 +31,94 @@ function removeLastClick() { } function handleMorphdomUpdate(text) { + // Track closed blocks + const closedBlocks = new Set(); + document.querySelectorAll(".thinking-block").forEach(block => { + const blockId = block.getAttribute("data-block-id"); + // If block exists and is not open, add to closed set + if (blockId && !block.hasAttribute("open")) { + closedBlocks.add(blockId); + } + }); + + // Store scroll positions for any open blocks + const scrollPositions = {}; + document.querySelectorAll(".thinking-block[open]").forEach(block => { + const content = block.querySelector(".thinking-content"); + const blockId = block.getAttribute("data-block-id"); + if (content && blockId) { + const isAtBottom = Math.abs((content.scrollHeight - content.scrollTop) - content.clientHeight) < 5; + scrollPositions[blockId] = { + position: content.scrollTop, + isAtBottom: isAtBottom + }; + } + }); + morphdom( document.getElementById("chat").parentNode, "
content is the same, preserve the entire element
toEl.className = fromEl.className;
toEl.innerHTML = fromEl.innerHTML;
- return false; // Skip updating the element
+ return false;
+ }
+ }
+
+ // For thinking blocks, respect closed state
+ if (fromEl.classList && fromEl.classList.contains("thinking-block") &&
+ toEl.classList && toEl.classList.contains("thinking-block")) {
+ const blockId = toEl.getAttribute("data-block-id");
+ // If this block was closed by user, keep it closed
+ if (blockId && closedBlocks.has(blockId)) {
+ toEl.removeAttribute("open");
+ }
+ }
+
+ return !fromEl.isEqualNode(toEl);
+ },
+
+ onElUpdated: function(el) {
+ // Restore scroll positions for open thinking blocks
+ if (el.classList && el.classList.contains("thinking-block") && el.hasAttribute("open")) {
+ const blockId = el.getAttribute("data-block-id");
+ const content = el.querySelector(".thinking-content");
+
+ if (content && blockId && scrollPositions[blockId]) {
+ setTimeout(() => {
+ if (scrollPositions[blockId].isAtBottom) {
+ content.scrollTop = content.scrollHeight;
+ } else {
+ content.scrollTop = scrollPositions[blockId].position;
+ }
+ }, 0);
}
}
- return !fromEl.isEqualNode(toEl); // Update only if nodes differ
}
}
);
+
+ // Add toggle listeners for new blocks
+ document.querySelectorAll(".thinking-block").forEach(block => {
+ if (!block._hasToggleListener) {
+ block.addEventListener("toggle", function(e) {
+ if (this.open) {
+ const content = this.querySelector(".thinking-content");
+ if (content) {
+ setTimeout(() => {
+ content.scrollTop = content.scrollHeight;
+ }, 0);
+ }
+ }
+ });
+ block._hasToggleListener = true;
+ }
+ });
}
diff --git a/modules/chat.py b/modules/chat.py
index fd949907..94d90bdc 100644
--- a/modules/chat.py
+++ b/modules/chat.py
@@ -417,16 +417,8 @@ def generate_chat_reply(text, state, regenerate=False, _continue=False, loading_
yield history
return
- show_after = html.escape(state.get("show_after")) if state.get("show_after") else None
for history in chatbot_wrapper(text, state, regenerate=regenerate, _continue=_continue, loading_message=loading_message, for_ui=for_ui):
- if show_after:
- after = history["visible"][-1][1].partition(show_after)[2] or "*Is thinking...*"
- yield {
- 'internal': history['internal'],
- 'visible': history['visible'][:-1] + [[history['visible'][-1][0], after]]
- }
- else:
- yield history
+ yield history
def character_is_loaded(state, raise_exception=False):
diff --git a/modules/html_generator.py b/modules/html_generator.py
index 5227e87e..a72e4859 100644
--- a/modules/html_generator.py
+++ b/modules/html_generator.py
@@ -107,8 +107,87 @@ def replace_blockquote(m):
return m.group().replace('\n', '\n> ').replace('\\begin{blockquote}', '').replace('\\end{blockquote}', '')
+def extract_thinking_block(string):
+ """Extract thinking blocks from the beginning of a string."""
+ if not string:
+ return None, string
+
+ THINK_START_TAG = "<think>"
+ THINK_END_TAG = "</think>"
+
+ # Look for opening tag
+ start_pos = string.lstrip().find(THINK_START_TAG)
+ if start_pos == -1:
+ return None, string
+
+ # Adjust start position to account for any leading whitespace
+ start_pos = string.find(THINK_START_TAG)
+
+ # Find the content after the opening tag
+ content_start = start_pos + len(THINK_START_TAG)
+
+ # Look for closing tag
+ end_pos = string.find(THINK_END_TAG, content_start)
+
+ if end_pos != -1:
+ # Both tags found - extract content between them
+ thinking_content = string[content_start:end_pos]
+ remaining_content = string[end_pos + len(THINK_END_TAG):]
+ return thinking_content, remaining_content
+ else:
+ # Only opening tag found - everything else is thinking content
+ thinking_content = string[content_start:]
+ return thinking_content, ""
+
+
@functools.lru_cache(maxsize=None)
-def convert_to_markdown(string):
+def convert_to_markdown(string, message_id=None):
+ if not string:
+ return ""
+
+ # Use a default message ID if none provided
+ if message_id is None:
+ message_id = "unknown"
+
+ # Extract thinking block if present
+ thinking_content, remaining_content = extract_thinking_block(string)
+
+ # Process the main content
+ html_output = process_markdown_content(remaining_content)
+
+ # If thinking content was found, process it using the same function
+ if thinking_content is not None:
+ thinking_html = process_markdown_content(thinking_content)
+
+ # Generate unique ID for the thinking block
+ block_id = f"thinking-{message_id}-0"
+
+ # Check if thinking is complete or still in progress
+ is_streaming = not remaining_content
+ title_text = "Thinking..." if is_streaming else "Thought"
+
+ thinking_block = f'''
+
+
+
+ {title_text}
+
+
+
+ '''
+
+ # Prepend the thinking block to the message HTML
+ html_output = thinking_block + html_output
+
+ return html_output
+
+
+def process_markdown_content(string):
+ """Process a string through the markdown conversion pipeline."""
if not string:
return ""
@@ -209,15 +288,15 @@ def convert_to_markdown(string):
return html_output
-def convert_to_markdown_wrapped(string, use_cache=True):
+def convert_to_markdown_wrapped(string, message_id=None, use_cache=True):
'''
Used to avoid caching convert_to_markdown calls during streaming.
'''
if use_cache:
- return convert_to_markdown(string)
+ return convert_to_markdown(string, message_id=message_id)
- return convert_to_markdown.__wrapped__(string)
+ return convert_to_markdown.__wrapped__(string, message_id=message_id)
def generate_basic_html(string):
@@ -273,7 +352,7 @@ def generate_instruct_html(history):
for i in range(len(history['visible'])):
row_visible = history['visible'][i]
row_internal = history['internal'][i]
- converted_visible = [convert_to_markdown_wrapped(entry, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
+ converted_visible = [convert_to_markdown_wrapped(entry, message_id=i, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
if converted_visible[0]: # Don't display empty user messages
output += (
@@ -320,7 +399,7 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
for i in range(len(history['visible'])):
row_visible = history['visible'][i]
row_internal = history['internal'][i]
- converted_visible = [convert_to_markdown_wrapped(entry, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
+ converted_visible = [convert_to_markdown_wrapped(entry, message_id=i, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
if converted_visible[0]: # Don't display empty user messages
output += (
@@ -360,7 +439,7 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
for i in range(len(history['visible'])):
row_visible = history['visible'][i]
row_internal = history['internal'][i]
- converted_visible = [convert_to_markdown_wrapped(entry, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
+ converted_visible = [convert_to_markdown_wrapped(entry, message_id=i, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
if converted_visible[0]: # Don't display empty user messages
output += (
diff --git a/modules/shared.py b/modules/shared.py
index b4f87082..5177ac67 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -59,7 +59,6 @@ settings = {
'seed': -1,
'custom_stopping_strings': '',
'custom_token_bans': '',
- 'show_after': '',
'negative_prompt': '',
'autoload_model': False,
'dark_theme': True,
diff --git a/modules/ui.py b/modules/ui.py
index 19b76cee..3e1bf6d8 100644
--- a/modules/ui.py
+++ b/modules/ui.py
@@ -207,7 +207,6 @@ def list_interface_input_elements():
'sampler_priority',
'custom_stopping_strings',
'custom_token_bans',
- 'show_after',
'negative_prompt',
'dry_sequence_breakers',
'grammar_string',
diff --git a/modules/ui_parameters.py b/modules/ui_parameters.py
index c3245a9d..b494a758 100644
--- a/modules/ui_parameters.py
+++ b/modules/ui_parameters.py
@@ -93,7 +93,6 @@ def create_ui(default_preset):
shared.gradio['sampler_priority'] = gr.Textbox(value=generate_params['sampler_priority'], lines=12, label='Sampler priority', info='Parameter names separated by new lines or commas.', elem_classes=['add_scrollbar'])
shared.gradio['custom_stopping_strings'] = gr.Textbox(lines=2, value=shared.settings["custom_stopping_strings"] or None, label='Custom stopping strings', info='Written between "" and separated by commas.', placeholder='"\\n", "\\nYou:"')
shared.gradio['custom_token_bans'] = gr.Textbox(value=shared.settings['custom_token_bans'] or None, label='Token bans', info='Token IDs to ban, separated by commas. The IDs can be found in the Default or Notebook tab.')
- shared.gradio['show_after'] = gr.Textbox(value=shared.settings['show_after'] or None, label='Show after', info='Hide the reply before this text.', placeholder="")
shared.gradio['negative_prompt'] = gr.Textbox(value=shared.settings['negative_prompt'], label='Negative prompt', info='For CFG. Only used when guidance_scale is different than 1.', lines=3, elem_classes=['add_scrollbar'])
shared.gradio['dry_sequence_breakers'] = gr.Textbox(value=generate_params['dry_sequence_breakers'], label='dry_sequence_breakers', info='Tokens across which sequence matching is not continued. Specified as a comma-separated list of quoted strings.')
with gr.Row() as shared.gradio['grammar_file_row']:
diff --git a/settings-template.yaml b/settings-template.yaml
index 94a5c034..83764f97 100644
--- a/settings-template.yaml
+++ b/settings-template.yaml
@@ -29,7 +29,6 @@ truncation_length: 8192
seed: -1
custom_stopping_strings: ''
custom_token_bans: ''
-show_after: ''
negative_prompt: ''
autoload_model: false
dark_theme: true