diff --git a/backend/open_webui/apps/webui/models/chats.py b/backend/open_webui/apps/webui/models/chats.py index 04355e997..509dff9fe 100644 --- a/backend/open_webui/apps/webui/models/chats.py +++ b/backend/open_webui/apps/webui/models/chats.py @@ -61,6 +61,9 @@ class ChatModel(BaseModel): class ChatForm(BaseModel): chat: dict +class ChatTitleMessagesForm(BaseModel): + title: str + messages: list[dict] class ChatTitleForm(BaseModel): title: str diff --git a/backend/open_webui/apps/webui/routers/utils.py b/backend/open_webui/apps/webui/routers/utils.py index 82c294bd7..bab30aff6 100644 --- a/backend/open_webui/apps/webui/routers/utils.py +++ b/backend/open_webui/apps/webui/routers/utils.py @@ -1,16 +1,14 @@ -import site -from pathlib import Path - import black import markdown + +from open_webui.apps.webui.models.chats import ChatTitleMessagesForm from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT -from open_webui.env import FONTS_DIR from open_webui.constants import ERROR_MESSAGES from fastapi import APIRouter, Depends, HTTPException, Response, status -from fpdf import FPDF from pydantic import BaseModel from starlette.responses import FileResponse from open_webui.utils.misc import get_gravatar_url +from open_webui.utils.pdf_generator import PDFGenerator from open_webui.utils.utils import get_admin_user router = APIRouter() @@ -56,58 +54,10 @@ class ChatForm(BaseModel): @router.post("/pdf") async def download_chat_as_pdf( - form_data: ChatForm, + form_data: ChatTitleMessagesForm, ): - global FONTS_DIR - - pdf = FPDF() - pdf.add_page() - - # When running using `pip install` the static directory is in the site packages. - if not FONTS_DIR.exists(): - FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts" - # When running using `pip install -e .` the static directory is in the site packages. - # This path only works if `open-webui serve` is run from the root of this project. - if not FONTS_DIR.exists(): - FONTS_DIR = Path("./backend/static/fonts") - - pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf") - pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf") - pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf") - pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf") - pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf") - pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf") - - pdf.set_font("NotoSans", size=12) - pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"]) - - pdf.set_auto_page_break(auto=True, margin=15) - - # Adjust the effective page width for multi_cell - effective_page_width = ( - pdf.w - 2 * pdf.l_margin - 10 - ) # Subtracted an additional 10 for extra padding - - # Add chat messages - for message in form_data.messages: - role = message["role"] - content = message["content"] - pdf.set_font("NotoSans", "B", size=14) # Bold for the role - pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L") - pdf.ln(1) # Extra space between messages - - pdf.set_font("NotoSans", size=10) # Regular for content - pdf.multi_cell(effective_page_width, 6, content, 0, "L") - pdf.ln(1.5) # Extra space between messages - - # Save the pdf with name .pdf - pdf_bytes = pdf.output() - - return Response( - content=bytes(pdf_bytes), - media_type="application/pdf", - headers={"Content-Disposition": "attachment;filename=chat.pdf"}, - ) + response = PDFGenerator(form_data).generate_chat_pdf() + return response @router.get("/db/download") diff --git a/backend/open_webui/static/assets/pdf-style.css b/backend/open_webui/static/assets/pdf-style.css new file mode 100644 index 000000000..bc83c3c64 --- /dev/null +++ b/backend/open_webui/static/assets/pdf-style.css @@ -0,0 +1,283 @@ +/* HTML and Body */ +html { + box-sizing: border-box; + font-size: 14px; /* Default font size */ + line-height: 1.5; +} + +*, *::before, *::after { + box-sizing: inherit; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: #212529; + background-color: #fff; + width: auto; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 500; + margin: 0; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +/* Grid System */ +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +/* Utilities */ +.text-center { + text-align: center; +} + +/* Additional Text Utilities */ +.text-muted { + color: #6c757d; /* Muted text color */ +} + +/* Small Text */ +small { + font-size: 80%; /* Smaller font size relative to the base */ + color: #6c757d; /* Lighter text color for secondary information */ + margin-bottom: 0; + margin-top: 0; +} + +/* Strong Element Styles */ +strong { + font-weight: bolder; /* Ensures the text is bold */ + color: inherit; /* Inherits the color from its parent element */ +} + +/* link */ +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +/* General styles for lists */ +ol, ul, li { + padding-left: 40px; /* Increase padding to move bullet points to the right */ + margin-left: 20px; /* Indent lists from the left */ +} + +/* Ordered list styles */ +ol { + list-style-type: decimal; /* Use numbers for ordered lists */ + margin-bottom: 10px; /* Space after each list */ +} + + +ol li { + margin-bottom: 0.5rem; /* Space between ordered list items */ +} + +/* Unordered list styles */ +ul { + list-style-type: disc; /* Use bullets for unordered lists */ + margin-bottom: 10px; /* Space after each list */ +} + +ul li { + margin-bottom: 0.5rem; /* Space between unordered list items */ +} + +/* List item styles */ +li { + margin-bottom: 5px; /* Space between list items */ + line-height: 1.5; /* Line height for better readability */ +} + +/* Nested lists */ +ol ol, ol ul, ul ol, ul ul { + padding-left: 20px; + margin-left: 30px; /* Further indent nested lists */ + margin-bottom: 0; /* Remove extra margin at the bottom of nested lists */ +} + +/* Code blocks */ +pre { + background-color: #f4f4f4; + padding: 10px; + overflow-x: auto; + max-width: 100%; /* Ensure it doesn't overflow the page */ + width: 80%; /* Set a specific width for a container-like appearance */ + margin: 0 1em; /* Center the pre block */ + box-sizing: border-box; /* Include padding in the width */ + border: 1px solid #ccc; /* Optional: Add a border for better definition */ + border-radius: 4px; /* Optional: Add rounded corners */ +} + +code { + font-family: 'Courier New', Courier, monospace; + background-color: #f4f4f4; + padding: 2px 4px; + border-radius: 4px; + box-sizing: border-box; /* Include padding in the width */ +} + +.message { + margin-top: 8px; + margin-bottom: 8px; +} + +/* Table Styles */ +table { + width: 100%; + margin-bottom: 1rem; + color: #212529; + border-collapse: collapse; /* Removes the space between borders */ +} + +th, td { + margin: 0; + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +tbody + tbody { + border-top: 2px solid #dee2e6; +} + +/* markdown-section styles */ +.markdown-section blockquote, +.markdown-section h1, +.markdown-section h2, +.markdown-section h3, +.markdown-section h4, +.markdown-section h5, +.markdown-section h6, +.markdown-section p, +.markdown-section pre, +.markdown-section table, +.markdown-section ul { + /* Give most block elements margin top and bottom */ + margin-top: 1rem; +} + +/* Remove top margin if it's the first child */ +.markdown-section blockquote:first-child, +.markdown-section h1:first-child, +.markdown-section h2:first-child, +.markdown-section h3:first-child, +.markdown-section h4:first-child, +.markdown-section h5:first-child, +.markdown-section h6:first-child, +.markdown-section p:first-child, +.markdown-section pre:first-child, +.markdown-section table:first-child, +.markdown-section ul:first-child { + margin-top: 0; +} + + +/* Remove top margin of