[{"id":"artifacts","user_id":"942d0349-0aca-4bf7-aad9-94489c71ce32","name":"Artifacts","type":"filter","content":"\"\"\"\ntitle: OpenWebUI Artifacts\nauthor: open-webui, atgehrhardt\nauthor_url: https://github.com/atgehrhardt\nfunding_url: https://github.com/open-webui\nversion: 1.2.7\nrequired_open_webui_version: 0.3.10\n\"\"\"\n\nimport os\nimport re\nimport uuid\nimport html\nimport traceback\nimport json\nfrom typing import Optional, List, Dict\nfrom pydantic import BaseModel, Field\nfrom bs4 import BeautifulSoup\nfrom apps.webui.models.files import Files, FileForm\nfrom config import UPLOAD_DIR\n\n\nclass MiddlewareHTMLGenerator:\n @staticmethod\n def generate_style():\n return \"\"\"\n body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #1e1e1e; color: #ffffff; }\n .header { height: 40px; background-color: #2d2d2d; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; position: sticky; top: 0; z-index: 1000; }\n .content-wrapper { padding: 20px; }\n .content-item { width: 100%; margin-bottom: 20px; border: 1px solid #444; background-color: #2d2d2d; }\n .content-item.code-view { padding: 10px; }\n .render-view .rendered-content { margin: 0; padding: 0; }\n pre { white-space: pre-wrap; word-wrap: break-word; background-color: #1e1e1e; padding: 10px; border-radius: 5px; margin: 0; }\n code { font-family: 'Courier New', Courier, monospace; }\n .hidden { display: none; }\n h2 { margin: 0; padding: 10px; background-color: #3d3d3d; }\n .iframe-wrapper { width: 100%; height: 600px; overflow: hidden; position: relative; resize: both; background-color: transparent; }\n .content-frame { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; background-color: transparent; }\n .resize-handle { position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; cursor: se-resize; }\n .responsive-controls { display: flex; justify-content: center; margin-bottom: 10px; margin-top: 15px; }\n .device-button { margin: 0 5px; padding: 5px 10px; background-color: transparent; color: #ffffff; border: 1px solid #ffffff; cursor: pointer; border-radius: 4px; transition: background-color 0.3s, color 0.3s; }\n .device-button:hover { background-color: rgba(255, 255, 255, 0.1); }\n .device-button.active { background-color: rgba(255, 255, 255, 0.2); font-weight: bold; }\n .switch { position: relative; display: inline-block; width: 60px; height: 24px; }\n .switch input { opacity: 0; width: 0; height: 0; }\n .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }\n .slider:before { position: absolute; content: \"\"; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }\n input:checked + .slider { background-color: #2196F3; }\n input:checked + .slider:before { transform: translateX(36px); }\n .slider-text { position: absolute; color: white; top: 50%; transform: translateY(-50%); text-align: center; left: 0; right: 0; font-size: 12px; }\n .nav-buttons { display: flex; align-items: center; }\n .nav-button, .select-button, .fullscreen-button { background-color: transparent; border: none; color: #ffffff; cursor: pointer; font-size: 18px; padding: 5px; margin: 0 5px; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 50%; transition: background-color 0.3s ease; }\n .select-button { border: none; padding: 0; }\n .select-button svg { width: 30px; height: 30px; }\n .nav-button:hover, .select-button:hover, .fullscreen-button:hover { background-color: rgba(255, 255, 255, 0.1); }\n .nav-button:disabled { color: #666666; cursor: not-allowed; }\n .nav-button:disabled:hover { background-color: transparent; }\n .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); }\n .modal-content { background-color: #2d2d2d; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 800px; border-radius: 5px; max-height: 80vh; overflow-y: auto; }\n .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }\n .close:hover, .close:focus { color: #fff; text-decoration: none; cursor: pointer; }\n .artifact-list { list-style-type: none; padding: 0; }\n .artifact-list li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #444; cursor: pointer; }\n .artifact-list li:hover { background-color: rgba(255, 255, 255, 0.1); }\n .artifact-info { flex: 1; margin-right: 10px; }\n .artifact-preview { width: 200px; height: 120px; overflow: hidden; background-color: transparent; }\n .artifact-preview iframe { width: 400px; height: 240px; border: none; transform: scale(0.5); transform-origin: top left; pointer-events: none; }\n .editor { width: 100%; height: 300px; font-family: monospace; font-size: 14px; border: 1px solid #444; background-color: #1e1e1e; color: #ffffff; padding: 10px; box-sizing: border-box; overflow: auto;white-space: pre-wrap;word-wrap: break-word;}\n .copy-button { position: absolute; top: 10px; right: 10px; background-color: #5E5B5A; border: none; color: white; padding: 5px 10px; text-align: center; text-decoration: none; display: inline-block; font-size: 14px; margin: 4px 2px; cursor: pointer; border-radius: 4px; }\n .copy-button:hover { background-color: #45a049; }\n .code-container { position: relative; }\n .iframe-wrapper:-webkit-full-screen { width: 100%; height: 100%; }\n .iframe-wrapper:-moz-full-screen { width: 100%; height: 100%; }\n .iframe-wrapper:-ms-fullscreen { width: 100%; height: 100%; }\n .iframe-wrapper:fullscreen { width: 100%; height: 100%; }\n \"\"\"\n\n @staticmethod\n def generate_script():\n return \"\"\"\n const totalArtifacts = document.querySelectorAll('.render-view').length;\n let currentArtifact = 1;\n let isCodeView = false;\n\n const modal = document.getElementById(\"artifactModal\");\n const selectButton = document.getElementById(\"selectArtifact\");\n const closeButton = document.getElementsByClassName(\"close\")[0];\n const artifactList = document.getElementById(\"artifactList\");\n const fullscreenButton = document.getElementById('fullscreenButton');\n const body = document.body;\n\n window.addEventListener('load', () => {\n for (let i = 0; i < totalArtifacts; i++) {\n ['html', 'css', 'js'].forEach(type => {\n const storedContent = localStorage.getItem(`artifact_${i}_${type}`);\n if (storedContent) {\n const editor = document.getElementById(`${type}-editor-${i}`);\n if (editor) {\n editor.value = storedContent;\n }\n }\n });\n }\n reloadCurrentArtifact();\n });\n\n function applyStoredChanges(artifactNumber) {\n ['html', 'css', 'js'].forEach(type => {\n const storedContent = localStorage.getItem(`artifact_${artifactNumber - 1}_${type}`);\n if (storedContent) {\n updateContent(type, artifactNumber - 1, true);\n }\n });\n }\n\n document.getElementById('toggleView').addEventListener('change', function() {\n isCodeView = this.checked;\n const sliderText = document.querySelector('.slider-text');\n sliderText.textContent = isCodeView ? 'Code' : 'Render';\n updateArtifactVisibility();\n });\n\n function updateArtifactVisibility() {\n document.querySelectorAll('.content-item').forEach(item => {\n const isCorrectArtifact = item.dataset.artifact == currentArtifact;\n const isCorrectView = (item.classList.contains('render-view') && !isCodeView) || \n (item.classList.contains('code-view') && isCodeView);\n item.classList.toggle('hidden', !(isCorrectArtifact && isCorrectView));\n });\n document.getElementById('prevArtifact').disabled = currentArtifact === 1;\n document.getElementById('nextArtifact').disabled = currentArtifact === totalArtifacts;\n }\n\n function navigateToArtifact(artifactNumber) {\n currentArtifact = artifactNumber;\n updateArtifactVisibility();\n reloadCurrentArtifact();\n modal.style.display = \"none\";\n }\n\n function reloadCurrentArtifact() {\n const frame = document.querySelector(`.content-item[data-artifact=\"${currentArtifact}\"] .content-frame`);\n if (frame) {\n const currentSrcdoc = frame.getAttribute('data-original-content');\n frame.srcdoc = '';\n setTimeout(() => {\n frame.srcdoc = currentSrcdoc;\n }, 0);\n }\n }\n\n document.getElementById('prevArtifact').addEventListener('click', () => {\n if (currentArtifact > 1) {\n currentArtifact--;\n updateArtifactVisibility();\n reloadCurrentArtifact();\n }\n });\n\n document.getElementById('nextArtifact').addEventListener('click', () => {\n if (currentArtifact < totalArtifacts) {\n currentArtifact++;\n updateArtifactVisibility();\n reloadCurrentArtifact();\n }\n });\n\n function updateContent(type, index, skipReload = false) {\n const frame = document.querySelector(`.content-item[data-artifact=\"${index + 1}\"] .content-frame`);\n const editor = document.getElementById(`${type}-editor-${index}`);\n const content = editor.value;\n \n let updatedSrcdoc = frame.getAttribute('data-original-content');\n const parser = new DOMParser();\n const doc = parser.parseFromString(updatedSrcdoc, 'text/html');\n \n if (type === 'html') {\n doc.body.innerHTML = content;\n } else if (type === 'css') {\n let styleTag = doc.querySelector('style');\n if (!styleTag) {\n styleTag = doc.createElement('style');\n doc.head.appendChild(styleTag);\n }\n styleTag.textContent = content;\n } else if (type === 'js') {\n let scriptTag = doc.querySelector('script:not([src])');\n if (!scriptTag) {\n scriptTag = doc.createElement('script');\n doc.body.appendChild(scriptTag);\n }\n scriptTag.textContent = content;\n }\n \n updatedSrcdoc = new XMLSerializer().serializeToString(doc);\n \n frame.setAttribute('data-original-content', updatedSrcdoc);\n \n if (!skipReload) {\n frame.srcdoc = '';\n setTimeout(() => {\n frame.srcdoc = updatedSrcdoc;\n }, 0);\n }\n \n localStorage.setItem(`artifact_${index}_${type}`, content);\n console.log(`Content updated for artifact ${index + 1}, type ${type}`);\n }\n\n function copyToClipboard(button, elementId) {\n const codeElement = document.getElementById(elementId);\n const textArea = document.createElement('textarea');\n textArea.value = codeElement.textContent;\n document.body.appendChild(textArea);\n textArea.select();\n document.execCommand('copy');\n document.body.removeChild(textArea);\n \n const originalText = button.textContent;\n button.textContent = 'Copied!';\n setTimeout(() => {\n button.textContent = originalText;\n }, 2000);\n }\n\n selectButton.onclick = function() {\n const makeTransparent = (doc) => {\n doc.body.style.background = 'transparent';\n const styleEl = doc.createElement('style');\n styleEl.textContent = 'body { background: transparent !important; }';\n doc.head.appendChild(styleEl);\n };\n\n artifactList.innerHTML = '';\n document.querySelectorAll('.content-frame').forEach((frame, index) => {\n const li = document.createElement('li');\n const previewContent = frame.getAttribute('srcdoc');\n \n li.innerHTML = `\n
\n Artifact ${index + 1}\n
\n
\n \n
\n `;\n li.onclick = function() { navigateToArtifact(index + 1); };\n artifactList.appendChild(li);\n\n const previewIframe = li.querySelector('.artifact-preview iframe');\n previewIframe.onload = function() {\n makeTransparent(this.contentDocument);\n this.contentDocument.body.style.transform = 'scale(0.5)';\n this.contentDocument.body.style.transformOrigin = 'top left';\n this.style.pointerEvents = 'none';\n };\n previewIframe.srcdoc = previewContent;\n });\n modal.style.display = \"block\";\n }\n\n closeButton.onclick = function() {\n modal.style.display = \"none\";\n }\n\n window.onclick = function(event) {\n if (event.target == modal) {\n modal.style.display = \"none\";\n }\n }\n\n document.querySelectorAll('.device-button').forEach(button => {\n button.addEventListener('click', function() {\n const width = this.getAttribute('data-width');\n const wrapper = this.closest('.content-item').querySelector('.iframe-wrapper');\n const iframe = wrapper.querySelector('.content-frame');\n \n if (width === '100%') {\n wrapper.style.width = '100%';\n wrapper.style.height = '600px';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n } else {\n wrapper.style.width = width;\n wrapper.style.height = '80vh';\n iframe.style.width = width;\n iframe.style.height = '100%';\n }\n \n this.closest('.responsive-controls').querySelectorAll('.device-button').forEach(btn => {\n btn.classList.remove('active');\n });\n this.classList.add('active');\n });\n });\n\n document.querySelectorAll('.resize-handle').forEach(handle => {\n handle.addEventListener('mousedown', initResize, false);\n });\n\n function initResize(e) {\n window.addEventListener('mousemove', resize, false);\n window.addEventListener('mouseup', stopResize, false);\n }\n\n function resize(e) {\n if (!body.classList.contains('fullscreen')) {\n const wrapper = e.target.closest('.iframe-wrapper');\n wrapper.style.width = (e.clientX - wrapper.offsetLeft) + 'px';\n wrapper.style.height = (e.clientY - wrapper.offsetTop) + 'px';\n }\n }\n\n function stopResize(e) {\n window.removeEventListener('mousemove', resize, false);\n window.removeEventListener('mouseup', stopResize, false);\n }\n\n function toggleFullscreen() {\n const currentFrame = document.querySelector(`.content-item[data-artifact=\"${currentArtifact}\"] .iframe-wrapper`);\n \n if (!document.fullscreenElement) {\n if (currentFrame.requestFullscreen) {\n currentFrame.requestFullscreen();\n } else if (currentFrame.mozRequestFullScreen) {\n currentFrame.mozRequestFullScreen();\n } else if (currentFrame.webkitRequestFullscreen) {\n currentFrame.webkitRequestFullscreen();\n } else if (currentFrame.msRequestFullscreen) {\n currentFrame.msRequestFullscreen();\n }\n } else {\n if (document.exitFullscreen) {\n document.exitFullscreen();\n } else if (document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if (document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n } else if (document.msExitFullscreen) {\n document.msExitFullscreen();\n }\n }\n }\n \n fullscreenButton.addEventListener('click', toggleFullscreen);\n\n document.addEventListener('fullscreenchange', updateFullscreenButtonIcon);\n document.addEventListener('webkitfullscreenchange', updateFullscreenButtonIcon);\n document.addEventListener('mozfullscreenchange', updateFullscreenButtonIcon);\n document.addEventListener('MSFullscreenChange', updateFullscreenButtonIcon);\n\n function updateFullscreenButtonIcon() {\n if (document.fullscreenElement) {\n fullscreenButton.innerHTML = `\n \n \n \n `;\n } else {\n fullscreenButton.innerHTML = `\n \n \n \n `;\n }\n }\n\n updateArtifactVisibility();\n \"\"\"\n\n @staticmethod\n def generate_content_item(i, page):\n html_content = page.get(\"html\", \"\")\n raw_html = page.get(\"raw_html\", \"\")\n css_content = page.get(\"css\", \"\")\n js_content = page.get(\"js\", \"\")\n\n escaped_html = html.escape(raw_html)\n escaped_css = html.escape(css_content)\n escaped_js = html.escape(js_content)\n\n base_html = f\"\"\"\n \n \n \n \n \n \n \n \n {html_content}\n \n \n \n \"\"\"\n escaped_base_html = html.escape(base_html)\n\n # CodePen data preparation\n codepen_data = {\"html\": html_content, \"css\": css_content, \"js\": js_content}\n # 对 JSONstring 进行 HTML 转义\n JSONstring = html.escape(json.dumps(codepen_data))\n\n return f\"\"\"\n
\n

HTML Content {i+1}

\n
\n \n \n \n \n
\n
\n \n \n
\n
\n
\n
\n \n
\n
\n
\n \n {\"\" if not css_content else f'''\n \n '''}\n {\"\" if not js_content else f'''\n \n '''}\n \"\"\"\n\n @classmethod\n def create_middleware_html(cls, pages):\n content_items = \"\".join(\n cls.generate_content_item(i, page) for i, page in enumerate(pages)\n )\n\n return f\"\"\"\n \n \n \n \n \n Generated Content\n \n \n \n
\n \n
\n \n \n \n \n
\n
\n
\n {content_items}\n
\n
\n
\n ×\n

Select Artifact

\n \n
\n
\n \n \n \n \"\"\"\n\n\nclass Filter:\n class Valves(BaseModel):\n priority: int = Field(\n default=0, description=\"Priority level for the filter operations.\"\n )\n\n def __init__(self):\n self.valves = self.Valves()\n self.viz_dir = \"visualizations\"\n self.html_dir = \"html\"\n self.middleware_file = \"middleware.html\"\n self.current_artifact = None\n\n def ensure_chat_directory(self, chat_id, content_type):\n chat_dir = os.path.join(UPLOAD_DIR, self.viz_dir, content_type, chat_id)\n os.makedirs(chat_dir, exist_ok=True)\n return chat_dir\n\n def extract_content(self, content, pattern):\n return re.findall(pattern, content, re.IGNORECASE | re.DOTALL)\n\n def write_content_to_file(self, content, user_id, chat_id, content_type):\n chat_dir = self.ensure_chat_directory(chat_id, content_type)\n filename = f\"{content_type}_{uuid.uuid4()}.html\"\n file_path = os.path.join(chat_dir, filename)\n\n with open(file_path, \"w\") as f:\n f.write(content)\n\n relative_path = os.path.join(self.viz_dir, content_type, chat_id, filename)\n file_form = FileForm(\n id=str(uuid.uuid4()),\n filename=relative_path,\n meta={\n \"name\": filename,\n \"content_type\": \"text/html\",\n \"size\": len(content),\n \"path\": file_path,\n },\n )\n return Files.insert_new_file(user_id, file_form).id\n\n def parse_content(self, content):\n html_pattern = r\"```(?:html|xml)\\s*([\\s\\S]*?)\\s*```\"\n css_pattern = r\"```css\\s*([\\s\\S]*?)\\s*```\"\n js_pattern = r\"```javascript\\s*([\\s\\S]*?)\\s*```\"\n svg_pattern = r\"\"\n\n html_blocks = self.extract_content(content, html_pattern)\n css_blocks = self.extract_content(content, css_pattern)\n js_blocks = self.extract_content(content, js_pattern)\n standalone_svg_blocks = self.extract_content(content, svg_pattern)\n\n artifacts = []\n for i in range(\n max(\n len(html_blocks),\n len(css_blocks),\n len(js_blocks),\n len(standalone_svg_blocks),\n )\n ):\n artifact = {\"html\": \"\", \"css\": \"\", \"js\": \"\", \"raw_html\": \"\"}\n if i < len(html_blocks):\n artifact[\"html\"] = html_blocks[i]\n artifact[\"raw_html\"] = html_blocks[i]\n if i < len(css_blocks):\n artifact[\"css\"] = css_blocks[i]\n if i < len(js_blocks):\n artifact[\"js\"] = js_blocks[i]\n if i < len(standalone_svg_blocks):\n artifact[\"html\"] = standalone_svg_blocks[i]\n artifact[\"raw_html\"] = standalone_svg_blocks[i]\n artifacts.append(artifact)\n\n return artifacts\n\n def create_middleware_html(self, pages):\n return MiddlewareHTMLGenerator.create_middleware_html(pages)\n\n def inlet(self, body: dict, user: Optional[dict] = None) -> dict:\n return body\n\n def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n if \"messages\" in body and body[\"messages\"] and __user__ and \"id\" in __user__:\n last_message = body[\"messages\"][-1][\"content\"]\n chat_id = body.get(\"chat_id\")\n\n if chat_id:\n try:\n pages = self.parse_content(last_message)\n\n if pages:\n middleware_content = self.create_middleware_html(pages)\n middleware_id = self.write_content_to_file(\n middleware_content,\n __user__[\"id\"],\n chat_id,\n self.html_dir,\n )\n\n body[\"messages\"][-1][\n \"content\"\n ] += f\"\\n\\n{{{{HTML_FILE_ID_{middleware_id}}}}}\"\n\n except Exception as e:\n error_msg = (\n f\"Error processing content: {str(e)}\\n{traceback.format_exc()}\"\n )\n print(error_msg)\n body[\"messages\"][-1][\n \"content\"\n ] += f\"\\n\\nError: Failed to process content. Details: {error_msg}\"\n else:\n print(\"chat_id is missing\")\n\n return body\n","meta":{"description":"Parses all HTML, CSS, JavaScript, and SVG code in the LLM output and renders it in real time in your chats.","manifest":{"title":"OpenWebUI Artifacts","author":"open-webui, atgehrhardt","author_url":"https://github.com/atgehrhardt","funding_url":"https://github.com/open-webui","version":"1.2.7","required_open_webui_version":"0.3.10"}},"is_active":true,"is_global":true,"updated_at":1724579187,"created_at":1723386585}]