diff --git a/css/main.css b/css/main.css
index d7142336..6cb99fc3 100644
--- a/css/main.css
+++ b/css/main.css
@@ -592,6 +592,7 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* {
padding: 0.65rem 2.5rem;
border: 0;
box-shadow: 0;
+ border-radius: 8px;
}
#chat-input textarea::placeholder {
@@ -611,6 +612,16 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* {
display: none;
}
+#chat-input .submit-button {
+ display: none;
+}
+
+#chat-input .upload-button {
+ margin-right: 16px;
+ margin-bottom: 7px;
+ background: transparent;
+}
+
.chat-input-positioned {
max-width: 54rem;
left: 50%;
@@ -1395,3 +1406,48 @@ strong {
.dark #vram-info .value {
color: #07ff07;
}
+
+.message-attachments {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.attachment-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ background: rgb(0 0 0 / 5%);
+ border-radius: 6px;
+ border: 1px solid rgb(0 0 0 / 10%);
+ min-width: 80px;
+ max-width: 120px;
+}
+
+.attachment-icon {
+ margin-bottom: 4px;
+ color: #555;
+}
+
+.attachment-name {
+ font-size: 0.8em;
+ text-align: center;
+ word-break: break-word;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.dark .attachment-box {
+ background: rgb(255 255 255 / 5%);
+ border: 1px solid rgb(255 255 255 / 10%);
+}
+
+.dark .attachment-icon {
+ color: #ccc;
+}
diff --git a/modules/chat.py b/modules/chat.py
index 3efc55db..cdd50c92 100644
--- a/modules/chat.py
+++ b/modules/chat.py
@@ -157,7 +157,9 @@ def generate_chat_prompt(user_input, state, **kwargs):
impersonate = kwargs.get('impersonate', False)
_continue = kwargs.get('_continue', False)
also_return_rows = kwargs.get('also_return_rows', False)
- history = kwargs.get('history', state['history'])['internal']
+ history_data = kwargs.get('history', state['history'])
+ history = history_data['internal']
+ metadata = history_data.get('metadata', {})
# Templates
chat_template_str = state['chat_template_str']
@@ -196,11 +198,13 @@ def generate_chat_prompt(user_input, state, **kwargs):
messages.append({"role": "system", "content": context})
insert_pos = len(messages)
- for entry in reversed(history):
+ for i, entry in enumerate(reversed(history)):
user_msg = entry[0].strip()
assistant_msg = entry[1].strip()
tool_msg = entry[2].strip() if len(entry) > 2 else ''
+ row_idx = len(history) - i - 1
+
if tool_msg:
messages.insert(insert_pos, {"role": "tool", "content": tool_msg})
@@ -208,10 +212,40 @@ def generate_chat_prompt(user_input, state, **kwargs):
messages.insert(insert_pos, {"role": "assistant", "content": assistant_msg})
if user_msg not in ['', '<|BEGIN-VISIBLE-CHAT|>']:
- messages.insert(insert_pos, {"role": "user", "content": user_msg})
+ # Check for user message attachments in metadata
+ user_key = f"user_{row_idx}"
+ enhanced_user_msg = user_msg
+
+ # Add attachment content if present
+ if user_key in metadata and "attachments" in metadata[user_key]:
+ attachments_text = ""
+ for attachment in metadata[user_key]["attachments"]:
+ filename = attachment.get("name", "file")
+ content = attachment.get("content", "")
+ attachments_text += f"\nName: {filename}\nContents:\n\n=====\n{content}\n=====\n\n"
+
+ if attachments_text:
+ enhanced_user_msg = f"{user_msg}\n\nATTACHMENTS:{attachments_text}"
+
+ messages.insert(insert_pos, {"role": "user", "content": enhanced_user_msg})
user_input = user_input.strip()
if user_input and not impersonate and not _continue:
+ # For the current user input being processed, check if we need to add attachments
+ if not impersonate and not _continue and len(history_data.get('metadata', {})) > 0:
+ current_row_idx = len(history)
+ user_key = f"user_{current_row_idx}"
+
+ if user_key in metadata and "attachments" in metadata[user_key]:
+ attachments_text = ""
+ for attachment in metadata[user_key]["attachments"]:
+ filename = attachment.get("name", "file")
+ content = attachment.get("content", "")
+ attachments_text += f"\nName: {filename}\nContents:\n\n=====\n{content}\n=====\n\n"
+
+ if attachments_text:
+ user_input = f"{user_input}\n\nATTACHMENTS:{attachments_text}"
+
messages.append({"role": "user", "content": user_input})
def make_prompt(messages):
@@ -280,7 +314,6 @@ def generate_chat_prompt(user_input, state, **kwargs):
# Resort to truncating the user input
else:
-
user_message = messages[-1]['content']
# Bisect the truncation point
@@ -393,7 +426,74 @@ def add_message_version(history, row_idx, is_current=True):
history['metadata'][key]["current_version_index"] = len(history['metadata'][key]["versions"]) - 1
+def add_message_attachment(history, row_idx, file_path, is_user=True):
+ """Add a file attachment to a message in history metadata"""
+ if 'metadata' not in history:
+ history['metadata'] = {}
+
+ key = f"{'user' if is_user else 'assistant'}_{row_idx}"
+
+ if key not in history['metadata']:
+ history['metadata'][key] = {"timestamp": get_current_timestamp()}
+ if "attachments" not in history['metadata'][key]:
+ history['metadata'][key]["attachments"] = []
+
+ # Get file info using pathlib
+ path = Path(file_path)
+ filename = path.name
+ file_extension = path.suffix.lower()
+
+ try:
+ # Handle different file types
+ if file_extension == '.pdf':
+ # Process PDF file
+ content = extract_pdf_text(path)
+ file_type = "application/pdf"
+ else:
+ # Default handling for text files
+ with open(path, 'r', encoding='utf-8') as f:
+ content = f.read()
+ file_type = "text/plain"
+
+ # Add attachment
+ attachment = {
+ "name": filename,
+ "type": file_type,
+ "content": content,
+ }
+
+ history['metadata'][key]["attachments"].append(attachment)
+ return content # Return the content for reuse
+ except Exception as e:
+ logger.error(f"Error processing attachment {filename}: {e}")
+ return None
+
+
+def extract_pdf_text(pdf_path):
+ """Extract text from a PDF file"""
+ import PyPDF2
+
+ text = ""
+ try:
+ with open(pdf_path, 'rb') as file:
+ pdf_reader = PyPDF2.PdfReader(file)
+ for page_num in range(len(pdf_reader.pages)):
+ page = pdf_reader.pages[page_num]
+ text += page.extract_text() + "\n\n"
+
+ return text.strip()
+ except Exception as e:
+ logger.error(f"Error extracting text from PDF: {e}")
+ return f"[Error extracting PDF text: {str(e)}]"
+
+
def chatbot_wrapper(text, state, regenerate=False, _continue=False, loading_message=True, for_ui=False):
+ # Handle dict format with text and files
+ files = []
+ if isinstance(text, dict):
+ files = text.get('files', [])
+ text = text.get('text', '')
+
history = state['history']
output = copy.deepcopy(history)
output = apply_extensions('history', output)
@@ -411,12 +511,18 @@ def chatbot_wrapper(text, state, regenerate=False, _continue=False, loading_mess
if not (regenerate or _continue):
visible_text = html.escape(text)
+ # Process file attachments and store in metadata
+ row_idx = len(output['internal'])
+
+ # Add attachments to metadata only, not modifying the message text
+ for file_path in files:
+ add_message_attachment(output, row_idx, file_path, is_user=True)
+
# Apply extensions
text, visible_text = apply_extensions('chat_input', text, visible_text, state)
text = apply_extensions('input', text, state, is_chat=True)
# Current row index
- row_idx = len(output['internal'])
output['internal'].append([text, ''])
output['visible'].append([visible_text, ''])
# Add metadata with timestamp
@@ -1215,7 +1321,7 @@ def handle_replace_last_reply_click(text, state):
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
- return [history, html, ""]
+ return [history, html, {"text": "", "files": []}]
def handle_send_dummy_message_click(text, state):
@@ -1223,7 +1329,7 @@ def handle_send_dummy_message_click(text, state):
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
- return [history, html, ""]
+ return [history, html, {"text": "", "files": []}]
def handle_send_dummy_reply_click(text, state):
@@ -1231,7 +1337,7 @@ def handle_send_dummy_reply_click(text, state):
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
- return [history, html, ""]
+ return [history, html, {"text": "", "files": []}]
def handle_remove_last_click(state):
@@ -1239,7 +1345,7 @@ def handle_remove_last_click(state):
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
- return [history, html, last_input]
+ return [history, html, {"text": last_input, "files": []}]
def handle_unique_id_select(state):
diff --git a/modules/html_generator.py b/modules/html_generator.py
index 36b31ac5..f5e0b28f 100644
--- a/modules/html_generator.py
+++ b/modules/html_generator.py
@@ -338,6 +338,7 @@ remove_svg = ''''''
info_svg = ''''''
info_svg_small = ''''''
+attachment_svg = ''''''
copy_button = f''
branch_button = f''
@@ -357,6 +358,28 @@ def format_message_timestamp(history, role, index):
return ""
+def format_message_attachments(history, role, index):
+ """Get formatted HTML for message attachments if available"""
+ key = f"{role}_{index}"
+ if 'metadata' in history and key in history['metadata'] and 'attachments' in history['metadata'][key]:
+ attachments = history['metadata'][key]['attachments']
+ if not attachments:
+ return ""
+
+ attachments_html = '
'
+ for attachment in attachments:
+ attachments_html += (
+ f'
'
+ f'
{attachment_svg}
'
+ f'
{html.escape(attachment["name"])}
'
+ f'
'
+ )
+ attachments_html += '
'
+ return attachments_html
+
+ return ""
+
+
def actions_html(history, i, info_message=""):
return (f''
f'{copy_button}'
@@ -380,6 +403,10 @@ def generate_instruct_html(history):
user_timestamp = format_message_timestamp(history, "user", i)
assistant_timestamp = format_message_timestamp(history, "assistant", i)
+ # Get attachments
+ user_attachments = format_message_attachments(history, "user", i)
+ assistant_attachments = format_message_attachments(history, "assistant", i)
+
# Create info buttons for timestamps if they exist
info_message_user = ""
if user_timestamp != "":
@@ -399,6 +426,7 @@ def generate_instruct_html(history):
f'data-raw="{html.escape(row_internal[0], quote=True)}">'
f'
'
f'
{converted_visible[0]}
'
+ f'{user_attachments}'
f'
{copy_button}{info_message_user}
'
f'
'
f'
'
@@ -410,6 +438,7 @@ def generate_instruct_html(history):
f'data-index={i}>'
f''
f'
{converted_visible[1]}
'
+ f'{assistant_attachments}'
f'{actions_html(history, i, info_message_assistant)}'
f'
'
f''
@@ -442,6 +471,10 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
user_timestamp = format_message_timestamp(history, "user", i)
assistant_timestamp = format_message_timestamp(history, "assistant", i)
+ # Get attachments
+ user_attachments = format_message_attachments(history, "user", i)
+ assistant_attachments = format_message_attachments(history, "assistant", i)
+
if converted_visible[0]: # Don't display empty user messages
output += (
f''
f'
{name1}{user_timestamp}
'
f'
{converted_visible[0]}
'
+ f'{user_attachments}'
f'
{copy_button}
'
f'
'
f''
@@ -463,6 +497,7 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
f''
f'
{name2}{assistant_timestamp}
'
f'
{converted_visible[1]}
'
+ f'{assistant_attachments}'
f'{actions_html(history, i)}'
f'
'
f''
@@ -484,6 +519,10 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
user_timestamp = format_message_timestamp(history, "user", i)
assistant_timestamp = format_message_timestamp(history, "assistant", i)
+ # Get attachments
+ user_attachments = format_message_attachments(history, "user", i)
+ assistant_attachments = format_message_attachments(history, "assistant", i)
+
# Create info buttons for timestamps if they exist
info_message_user = ""
if user_timestamp != "":
@@ -503,6 +542,7 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
f'data-raw="{html.escape(row_internal[0], quote=True)}">'
f''
f'
{converted_visible[0]}
'
+ f'{user_attachments}'
f'
{copy_button}{info_message_user}
'
f'
'
f''
@@ -514,6 +554,7 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
f'data-index={i}>'
f''
f'
{converted_visible[1]}
'
+ f'{assistant_attachments}'
f'{actions_html(history, i, info_message_assistant)}'
f'
'
f''
diff --git a/modules/ui_chat.py b/modules/ui_chat.py
index 513a632b..f244113c 100644
--- a/modules/ui_chat.py
+++ b/modules/ui_chat.py
@@ -54,7 +54,7 @@ def create_ui():
gr.HTML(value='☰', elem_id='gr-hover')
with gr.Column(scale=10, elem_id='chat-input-container'):
- shared.gradio['textbox'] = gr.Textbox(label='', placeholder='Send a message', elem_id='chat-input', elem_classes=['add_scrollbar'])
+ shared.gradio['textbox'] = gr.MultimodalTextbox(label='', placeholder='Send a message', file_types=['text', '.pdf'], elem_id='chat-input', elem_classes=['add_scrollbar'])
shared.gradio['show_controls'] = gr.Checkbox(value=shared.settings['show_controls'], label='Show controls (Ctrl+S)', elem_id='show-controls')
shared.gradio['typing-dots'] = gr.HTML(value='
', label='typing', elem_id='typing-container')
@@ -186,7 +186,7 @@ def create_event_handlers():
shared.gradio['Generate'].click(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
- lambda x: (x, ''), gradio('textbox'), gradio('Chat input', 'textbox'), show_progress=False).then(
+ lambda x: (x, {"text": "", "files": []}), gradio('textbox'), gradio('Chat input', 'textbox'), show_progress=False).then(
lambda: None, None, None, js='() => document.getElementById("chat").parentNode.parentNode.parentNode.classList.add("_generating")').then(
chat.generate_chat_reply_wrapper, gradio(inputs), gradio('display', 'history'), show_progress=False).then(
None, None, None, js='() => document.getElementById("chat").parentNode.parentNode.parentNode.classList.remove("_generating")').then(
@@ -194,7 +194,7 @@ def create_event_handlers():
shared.gradio['textbox'].submit(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
- lambda x: (x, ''), gradio('textbox'), gradio('Chat input', 'textbox'), show_progress=False).then(
+ lambda x: (x, {"text": "", "files": []}), gradio('textbox'), gradio('Chat input', 'textbox'), show_progress=False).then(
lambda: None, None, None, js='() => document.getElementById("chat").parentNode.parentNode.parentNode.classList.add("_generating")').then(
chat.generate_chat_reply_wrapper, gradio(inputs), gradio('display', 'history'), show_progress=False).then(
None, None, None, js='() => document.getElementById("chat").parentNode.parentNode.parentNode.classList.remove("_generating")').then(
diff --git a/requirements/full/requirements.txt b/requirements/full/requirements.txt
index c65ab8a2..afb5f9d4 100644
--- a/requirements/full/requirements.txt
+++ b/requirements/full/requirements.txt
@@ -13,6 +13,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_amd.txt b/requirements/full/requirements_amd.txt
index 3da16d3e..46c33034 100644
--- a/requirements/full/requirements_amd.txt
+++ b/requirements/full/requirements_amd.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_amd_noavx2.txt b/requirements/full/requirements_amd_noavx2.txt
index 271b4bd0..c8e94cbd 100644
--- a/requirements/full/requirements_amd_noavx2.txt
+++ b/requirements/full/requirements_amd_noavx2.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_apple_intel.txt b/requirements/full/requirements_apple_intel.txt
index 15df937c..dc403ae2 100644
--- a/requirements/full/requirements_apple_intel.txt
+++ b/requirements/full/requirements_apple_intel.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_apple_silicon.txt b/requirements/full/requirements_apple_silicon.txt
index bd2f8339..5c643c4c 100644
--- a/requirements/full/requirements_apple_silicon.txt
+++ b/requirements/full/requirements_apple_silicon.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_cpu_only.txt b/requirements/full/requirements_cpu_only.txt
index 98c25649..ccabea84 100644
--- a/requirements/full/requirements_cpu_only.txt
+++ b/requirements/full/requirements_cpu_only.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_cpu_only_noavx2.txt b/requirements/full/requirements_cpu_only_noavx2.txt
index 6e13c1d2..7e9da47f 100644
--- a/requirements/full/requirements_cpu_only_noavx2.txt
+++ b/requirements/full/requirements_cpu_only_noavx2.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_noavx2.txt b/requirements/full/requirements_noavx2.txt
index 67a5cb73..fdf5cd0e 100644
--- a/requirements/full/requirements_noavx2.txt
+++ b/requirements/full/requirements_noavx2.txt
@@ -13,6 +13,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/full/requirements_nowheels.txt b/requirements/full/requirements_nowheels.txt
index 2e631bf0..22d39ded 100644
--- a/requirements/full/requirements_nowheels.txt
+++ b/requirements/full/requirements_nowheels.txt
@@ -12,6 +12,7 @@ peft==0.15.*
Pillow>=9.5.0
psutil
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements.txt b/requirements/portable/requirements.txt
index 409252f6..ec9bafc6 100644
--- a/requirements/portable/requirements.txt
+++ b/requirements/portable/requirements.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_apple_intel.txt b/requirements/portable/requirements_apple_intel.txt
index 89adbabf..025a737e 100644
--- a/requirements/portable/requirements_apple_intel.txt
+++ b/requirements/portable/requirements_apple_intel.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_apple_silicon.txt b/requirements/portable/requirements_apple_silicon.txt
index 0b1c03fa..32644e87 100644
--- a/requirements/portable/requirements_apple_silicon.txt
+++ b/requirements/portable/requirements_apple_silicon.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_cpu_only.txt b/requirements/portable/requirements_cpu_only.txt
index eb4319b7..bd5c1d9b 100644
--- a/requirements/portable/requirements_cpu_only.txt
+++ b/requirements/portable/requirements_cpu_only.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_cpu_only_noavx2.txt b/requirements/portable/requirements_cpu_only_noavx2.txt
index 0a60d4de..51f2b7d9 100644
--- a/requirements/portable/requirements_cpu_only_noavx2.txt
+++ b/requirements/portable/requirements_cpu_only_noavx2.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_noavx2.txt b/requirements/portable/requirements_noavx2.txt
index 652e9900..aad6bf5a 100644
--- a/requirements/portable/requirements_noavx2.txt
+++ b/requirements/portable/requirements_noavx2.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_nowheels.txt b/requirements/portable/requirements_nowheels.txt
index 6f9566ba..4c055426 100644
--- a/requirements/portable/requirements_nowheels.txt
+++ b/requirements/portable/requirements_nowheels.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_vulkan.txt b/requirements/portable/requirements_vulkan.txt
index c83d61c7..3d98d1b0 100644
--- a/requirements/portable/requirements_vulkan.txt
+++ b/requirements/portable/requirements_vulkan.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich
diff --git a/requirements/portable/requirements_vulkan_noavx2.txt b/requirements/portable/requirements_vulkan_noavx2.txt
index e69f3bdf..f954b8d2 100644
--- a/requirements/portable/requirements_vulkan_noavx2.txt
+++ b/requirements/portable/requirements_vulkan_noavx2.txt
@@ -4,6 +4,7 @@ jinja2==3.1.6
markdown
numpy==1.26.*
pydantic==2.8.2
+PyPDF2==3.0.1
pyyaml
requests
rich