mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2025-01-18 02:39:47 +00:00
Added an enhanced markdown renderer as a service for apps
This commit is contained in:
parent
578377f4e7
commit
de48601007
@ -260,6 +260,25 @@ async def lollms_js():
|
||||
data = file.read()
|
||||
return data
|
||||
|
||||
@router.get("/lollms_markdown_renderer", response_class=PlainTextResponse)
|
||||
async def lollms_markdown_renderer():
|
||||
# Define the path to the JSON file using pathlib
|
||||
file_path = Path(__file__).parent / "lollms_markdown_renderer.js"
|
||||
|
||||
# Read the JSON file
|
||||
with file_path.open('r') as file:
|
||||
data = file.read()
|
||||
return data
|
||||
|
||||
@router.get("/lollms_markdown_renderer_css")
|
||||
async def lollms_markdown_renderer_css():
|
||||
# Define the path to the CSS file using pathlib
|
||||
file_path = Path(__file__).parent / "lollms_markdown_renderer.css"
|
||||
|
||||
# Use FileResponse to serve the CSS file
|
||||
return FileResponse(file_path, media_type="text/css")
|
||||
|
||||
|
||||
@router.get("/template")
|
||||
async def lollms_js():
|
||||
return {
|
||||
|
@ -105,7 +105,6 @@ class LollmsClient {
|
||||
* @returns {Array} A list of tokens representing the tokenized prompt.
|
||||
*/
|
||||
const output = await axios.post("/lollms_tokenize", {"prompt": prompt});
|
||||
console.log(output.data.named_tokens)
|
||||
return output.data.named_tokens
|
||||
}
|
||||
async detokenize(tokensList) {
|
||||
@ -657,9 +656,76 @@ extractCodeBlocks(text) {
|
||||
|
||||
return codeBlocks;
|
||||
}
|
||||
/**
|
||||
* Updates the given code based on the provided query string.
|
||||
* The query string can contain two types of modifications:
|
||||
* 1. FULL_REWRITE: Completely replaces the original code with the new code.
|
||||
* 2. REPLACE: Replaces specific code snippets within the original code.
|
||||
*
|
||||
* @param {string} originalCode - The original code to be updated.
|
||||
* @param {string} queryString - The string containing the update instructions.
|
||||
* @returns {object} - An object with the following properties:
|
||||
* - updatedCode: The updated code.
|
||||
* - modifications: An array of objects representing the changes made, each with properties 'oldCode' and 'newCode'.
|
||||
* - hasQuery: A boolean indicating whether the queryString contained any valid queries.
|
||||
*/
|
||||
updateCode(originalCode, queryString) {
|
||||
const queries = queryString.split('# REPLACE\n');
|
||||
let updatedCode = originalCode;
|
||||
const modifications = [];
|
||||
|
||||
// Check if there's a FULL_REWRITE first
|
||||
const fullRewriteStart = queryString.indexOf('# FULL_REWRITE');
|
||||
if (fullRewriteStart !== -1) {
|
||||
const newCode = queryString.slice(fullRewriteStart + 14).trim();
|
||||
updatedCode = newCode;
|
||||
modifications.push({
|
||||
oldCode: originalCode,
|
||||
newCode
|
||||
});
|
||||
return {
|
||||
updatedCode,
|
||||
modifications,
|
||||
hasQuery: true
|
||||
};
|
||||
}
|
||||
|
||||
if (queries.length === 1 && queries[0].trim() === '') {
|
||||
console.log("No queries detected");
|
||||
return {
|
||||
updatedCode,
|
||||
modifications: [],
|
||||
hasQuery: false
|
||||
};
|
||||
}
|
||||
|
||||
for (const query of queries) {
|
||||
if (query.trim() === '') continue;
|
||||
|
||||
const originalCodeStart = query.indexOf('# ORIGINAL\n') + 11;
|
||||
const originalCodeEnd = query.indexOf('\n# SET\n');
|
||||
const oldCode = query.slice(originalCodeStart, originalCodeEnd);
|
||||
|
||||
const newCodeStart = query.indexOf('# SET\n') + 6;
|
||||
const newCode = query.slice(newCodeStart);
|
||||
|
||||
const modification = {
|
||||
oldCode: oldCode.trim(),
|
||||
newCode: newCode.trim()
|
||||
};
|
||||
modifications.push(modification);
|
||||
|
||||
updatedCode = updatedCode.replace(oldCode, newCode.trim());
|
||||
}
|
||||
|
||||
console.log("New code", updatedCode);
|
||||
return {
|
||||
updatedCode,
|
||||
modifications,
|
||||
hasQuery: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LOLLMSRAGClient {
|
||||
constructor(baseURL, apiKey) {
|
||||
|
236
endpoints/lollms_markdown_renderer.css
Normal file
236
endpoints/lollms_markdown_renderer.css
Normal file
@ -0,0 +1,236 @@
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
.fade-in.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
/* Dark mode styles */
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
.dark body {
|
||||
background-color: #1a202c;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark .bg-white {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
.dark .text-gray-900 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark .hover\:bg-gray-200:hover {
|
||||
background-color: #4a5568;
|
||||
}
|
||||
.dark .border {
|
||||
border-color: #4a5568;
|
||||
}
|
||||
.dark input, .dark select {
|
||||
background-color: #2d3748;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark button {
|
||||
background-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark button:hover {
|
||||
background-color: #718096;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
main {
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
#help-section {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#help-section h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
#help-section h3 {
|
||||
color: #553c9a;
|
||||
}
|
||||
#help-section .dark h3 {
|
||||
color: #d6bcfa;
|
||||
}
|
||||
#help-section p, #help-section li {
|
||||
line-height: 1.6;
|
||||
}
|
||||
#help-section .bg-purple-100 {
|
||||
background-color: rgba(237, 233, 254, 0.5);
|
||||
}
|
||||
#help-section .dark .bg-purple-900 {
|
||||
background-color: rgba(76, 29, 149, 0.5);
|
||||
}
|
||||
/* Updated styles for dark mode */
|
||||
.dark #help-section {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.dark #help-section h2 {
|
||||
color: #a78bfa;
|
||||
}
|
||||
.dark #help-section h3 {
|
||||
color: #d6bcfa;
|
||||
}
|
||||
.dark #help-section .bg-white {
|
||||
background-color: #1a202c;
|
||||
}
|
||||
.dark #help-section .text-gray-600,
|
||||
.dark #help-section .text-gray-700 {
|
||||
color: #cbd5e0;
|
||||
}
|
||||
.dark #help-section .text-purple-700,
|
||||
.dark #help-section .text-purple-800 {
|
||||
color: #e9d8fd;
|
||||
}
|
||||
.dark #help-section button {
|
||||
background-color: #8b5cf6;
|
||||
}
|
||||
.dark #help-section button:hover {
|
||||
background-color: #7c3aed;
|
||||
}
|
||||
/* Styles for rendered Markdown */
|
||||
.markdown-content h1 {
|
||||
font-size: 2em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.markdown-content h3 {
|
||||
font-size: 1.17em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.markdown-content p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin-left: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.markdown-content code {
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 1em;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.dark .markdown-content code {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
.dark .markdown-content pre {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
.ascii-art {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.code-block-wrapper {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
padding: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.language-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background-color: #4CAF50;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.copy-button svg {
|
||||
margin-right: 5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
pre.line-numbers {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
padding-right: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.code-line:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
400
endpoints/lollms_markdown_renderer.js
Normal file
400
endpoints/lollms_markdown_renderer.js
Normal file
@ -0,0 +1,400 @@
|
||||
// Requires importing:
|
||||
// <!-- For code highlighting -->
|
||||
// <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/default.min.css">
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>
|
||||
|
||||
// <!-- For LaTeX math rendering -->
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-mml-chtml.js"></script>
|
||||
|
||||
// <!-- For Mermaid graph rendering -->
|
||||
// <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
|
||||
|
||||
// <script src="https://cdn.tailwindcss.com"></script>
|
||||
// <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
// <script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script>
|
||||
|
||||
|
||||
// <!-- Prism CSS -->
|
||||
// <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism.min.css" rel="stylesheet" />
|
||||
|
||||
// <!-- Prism JS -->
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
|
||||
|
||||
// <!-- If you want additional languages, include them like this -->
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-python.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-javascript.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-markup.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-c.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-cpp.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-java.min.js"></script>
|
||||
// <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-latex.min.js"></script>
|
||||
// When served with lollms, just use <script src="/lollms_markdown_renderer"></script>
|
||||
// Don't forget to get the css too <link rel="stylesheet" href="/lollms_markdown_renderer.css">
|
||||
|
||||
// Make sure there is a global variable called mr that instanciate MarkdownRenderer
|
||||
// mr = new MarkdownRenderer()
|
||||
|
||||
|
||||
class MarkdownRenderer {
|
||||
async renderMermaidDiagrams(text) {
|
||||
const mermaidCodeRegex = /```mermaid\n([\s\S]*?)```/g;
|
||||
const matches = text.match(mermaidCodeRegex);
|
||||
|
||||
if (!matches) return text; // Return original text if no Mermaid code found
|
||||
|
||||
for (const match of matches) {
|
||||
const mermaidCode = match.replace(/```mermaid\n/, '').replace(/```$/, '');
|
||||
const uniqueId = 'mermaid-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
try {
|
||||
const result = await mermaid.render(uniqueId, mermaidCode);
|
||||
const htmlCode = `
|
||||
<div class="relative flex justify-center items-center mt-4 mb-4">
|
||||
<div class="mermaid-diagram" id="${uniqueId}" style="transform-origin: center; transition: transform 0.3s;">
|
||||
${result.svg}
|
||||
</div>
|
||||
<div class="absolute top-0 left-0 flex gap-1 p-1">
|
||||
<button onclick="zoomMermaid('${uniqueId}', 1.1)" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold p-1 rounded inline-flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 100 100">
|
||||
<circle cx="40" cy="40" r="25" stroke="black" stroke-width="5" fill="none" />
|
||||
<line x1="60" y1="60" x2="80" y2="80" stroke="black" stroke-width="5" />
|
||||
<line x1="50" y1="40" x2="30" y2="40" stroke="black" stroke-width="3" />
|
||||
<line x1="40" y1="30" x2="40" y2="50" stroke="black" stroke-width="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="zoomMermaid('${uniqueId}', 0.9)" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold p-1 rounded inline-flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 100 100">
|
||||
<circle cx="40" cy="40" r="25" stroke="black" stroke-width="5" fill="none" />
|
||||
<line x1="60" y1="60" x2="80" y2="80" stroke="black" stroke-width="5" />
|
||||
<line x1="50" y1="40" x2="30" y2="40" stroke="black" stroke-width="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="saveMermaidAsPNG('${uniqueId}')" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold p-1 rounded inline-flex items-center">
|
||||
PNG
|
||||
</button>
|
||||
<button onclick="saveMermaidAsSVG('${uniqueId}')" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold p-1 rounded inline-flex items-center">
|
||||
SVG
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
text = text.replace(match, htmlCode);
|
||||
} catch (error) {
|
||||
console.error('Mermaid rendering failed:', error);
|
||||
text = text.replace(match, `<div class="mermaid-error">Failed to render diagram</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async renderCodeBlocks(text) {
|
||||
if (typeof Prism === 'undefined') {
|
||||
throw new Error('Prism is not loaded. Please include Prism.js in your project.');
|
||||
}
|
||||
|
||||
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||
|
||||
const renderedText = await text.replace(codeBlockRegex, (match, language, code) => {
|
||||
language = language || 'plaintext';
|
||||
|
||||
if (!Prism.languages[language]) {
|
||||
console.warn(`Language '${language}' is not supported by Prism. Falling back to plaintext.`);
|
||||
language = 'plaintext';
|
||||
}
|
||||
|
||||
const highlightedCode = Prism.highlight(code.trim(), Prism.languages[language], language);
|
||||
|
||||
const lines = highlightedCode.split(/\r?\n/);
|
||||
const numberedLines = lines.map((line, index) =>
|
||||
`<span class="code-line"><span class="line-number">${index + 1}</span><span class="line-content">${line}</span></span>`
|
||||
).join('\n');
|
||||
|
||||
return `<div class="code-block-wrapper">
|
||||
<div class="code-block-header">
|
||||
<div class="language-label">${language}</div>
|
||||
<button class="copy-button" onclick="mr.copyCode(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre class="line-numbers"><code class="language-${language}">${numberedLines}</code></pre>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return renderedText;
|
||||
}
|
||||
|
||||
handleInlineCode(text) {
|
||||
return text.replace(/`([^`]+)`/g, function(match, code) {
|
||||
return `<b>${code}</b>`;
|
||||
});
|
||||
}
|
||||
|
||||
handleMathEquations(text) {
|
||||
return text.replace(/\\\[([\s\S]*?)\\\]|\$\$([\s\S]*?)\$\$|\$([^\n]+?)\$/g, function(match, p1, p2, p3) {
|
||||
const equation = p1 || p2 || p3;
|
||||
return '<span class="math">' + equation + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
async handleTables(text) {
|
||||
let alignments = [];
|
||||
let tableRows = [];
|
||||
let isInTable = false;
|
||||
let hasHeader = false;
|
||||
|
||||
// Process the text line by line
|
||||
text = text.split('\n').map(line => {
|
||||
// Check if the line is a table row
|
||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||
isInTable = true;
|
||||
const tableRow = line.trim().slice(1, -1); // Remove leading and trailing |
|
||||
const cells = tableRow.split('|').map(cell => cell.trim());
|
||||
|
||||
if (cells.every(cell => cell.match(/^:?-+:?$/))) {
|
||||
// This is the header separator row
|
||||
alignments = cells.map(cell => {
|
||||
if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
|
||||
if (cell.endsWith(':')) return 'right';
|
||||
return 'left';
|
||||
});
|
||||
hasHeader = true;
|
||||
return ''; // Remove separator row
|
||||
}
|
||||
|
||||
const cellType = !hasHeader ? 'th' : 'td';
|
||||
const renderedCells = cells.map((cell, cellIndex) =>
|
||||
`<${cellType} class="border px-4 py-2" style="text-align: ${alignments[cellIndex] || 'left'};">${cell}</${cellType}>`
|
||||
).join('');
|
||||
|
||||
tableRows.push(`<tr>${renderedCells}</tr>`);
|
||||
return ''; // Remove the original Markdown line
|
||||
} else if (isInTable) {
|
||||
// We've reached the end of the table
|
||||
isInTable = false;
|
||||
hasHeader = false;
|
||||
const tableContent = tableRows.join('');
|
||||
tableRows = []; // Reset for next table
|
||||
return `<table class="table-auto w-full border-collapse border border-gray-300">${tableContent}</table>`;
|
||||
}
|
||||
return line; // Return non-table lines unchanged
|
||||
}).join('\n');
|
||||
|
||||
// Handle case where table is at the end of the text
|
||||
if (isInTable) {
|
||||
const tableContent = tableRows.join('');
|
||||
text += `<table class="table-auto w-full border-collapse border border-gray-300">${tableContent}</table>`;
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
handleHeaders(text) {
|
||||
return text.replace(/^(#{1,6})\s+(.*?)$/gm, function(match, hashes, content) {
|
||||
const level = hashes.length;
|
||||
return '<h' + level + '>' + content + '</h' + level + '>';
|
||||
});
|
||||
}
|
||||
|
||||
handleBoldText(text) {
|
||||
return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
}
|
||||
|
||||
handleItalicText(text) {
|
||||
return text.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
}
|
||||
|
||||
handleLinks(text) {
|
||||
return text.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2">$1</a>');
|
||||
}
|
||||
|
||||
handleUnorderedLists(text) {
|
||||
return text.replace(/^\s*[-*+]\s+(.*?)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
}
|
||||
|
||||
handleOrderedLists(text) {
|
||||
return text.replace(/^\s*(\d+)\.\s+(.*?)$/gm, '<li>$2</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ol>$1</ol>');
|
||||
}
|
||||
|
||||
handleBlockquotes(text) {
|
||||
return text.replace(/^>\s+(.*?)$/gm, '<blockquote>$1</blockquote>');
|
||||
}
|
||||
|
||||
handleHorizontalRules(text) {
|
||||
return text.replace(/^(-{3,}|_{3,}|\*{3,})$/gm, '<hr>');
|
||||
}
|
||||
|
||||
handleParagraphs(text) {
|
||||
//return text.replace(/^(?!<[uo]l|<blockquote|<h\d|<pre|<hr|<table|<li|<button)(.+)$/gm, '<p class="mb-4">$1</p>');
|
||||
// No need to handle paragraphs separately, they will be handled as the remaining content
|
||||
return text;
|
||||
}
|
||||
|
||||
async renderMarkdown(text) {
|
||||
// Handle Mermaid graphs first
|
||||
text = await this.renderMermaidDiagrams(text);
|
||||
|
||||
// Handle code blocks with syntax highlighting and copy button
|
||||
text = await this.renderCodeBlocks(text);
|
||||
|
||||
// Handle inline code
|
||||
text = this.handleInlineCode(text);
|
||||
|
||||
// Handle LaTeX-style math equations
|
||||
text = this.handleMathEquations(text);
|
||||
|
||||
// Handle tables
|
||||
text = await this.handleTables(text);
|
||||
|
||||
// Handle headers
|
||||
text = this.handleHeaders(text);
|
||||
|
||||
// Handle bold text
|
||||
text = this.handleBoldText(text);
|
||||
|
||||
// Handle italic text
|
||||
text = this.handleItalicText(text);
|
||||
|
||||
// Handle links
|
||||
text = this.handleLinks(text);
|
||||
|
||||
// Handle unordered lists
|
||||
text = this.handleUnorderedLists(text);
|
||||
|
||||
// Handle ordered lists
|
||||
text = this.handleOrderedLists(text);
|
||||
|
||||
// Handle blockquotes
|
||||
text = this.handleBlockquotes(text);
|
||||
|
||||
// Handle horizontal rules
|
||||
text = this.handleHorizontalRules(text);
|
||||
|
||||
// Handle paragraphs
|
||||
text = this.handleParagraphs(text);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
initMermaid() {
|
||||
if (typeof mermaid !== 'undefined') {
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
} else {
|
||||
console.error('Mermaid library is not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
initPrism() {
|
||||
if (typeof Prism !== 'undefined') {
|
||||
// No further initialization needed
|
||||
} else {
|
||||
console.error('Prism library is not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for Mermaid and code block handling
|
||||
saveMermaidAsPNG(id) {
|
||||
const svg = document.querySelector(`#${id} svg`);
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// Create a canvas with a higher resolution
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Set a scale factor for higher resolution (e.g., 2 for double resolution)
|
||||
const scaleFactor = 2;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
canvas.width = img.width * scaleFactor;
|
||||
canvas.height = img.height * scaleFactor;
|
||||
|
||||
// Scale the context to draw the image at a higher resolution
|
||||
ctx.scale(scaleFactor, scaleFactor);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const pngFile = canvas.toDataURL("image/png");
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.download = "mermaid_diagram.png";
|
||||
downloadLink.href = pngFile;
|
||||
downloadLink.click();
|
||||
};
|
||||
img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgData)));
|
||||
}
|
||||
|
||||
saveMermaidAsSVG(id) {
|
||||
const svg = document.querySelector(`#${id} svg`);
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const svgBlob = new Blob([svgData], {type: "image/svg+xml;charset=utf-8"});
|
||||
const svgUrl = URL.createObjectURL(svgBlob);
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = svgUrl;
|
||||
downloadLink.download = "mermaid_diagram.svg";
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
|
||||
zoomMermaid(id, factor) {
|
||||
const diagram = document.getElementById(id);
|
||||
const currentScale = diagram.style.transform ? parseFloat(diagram.style.transform.replace('scale(', '').replace(')', '')) : 1;
|
||||
const newScale = currentScale * factor;
|
||||
diagram.style.transform = `scale(${newScale})`;
|
||||
}
|
||||
|
||||
copyCode(button) {
|
||||
const codeBlock = button.closest('.code-block-wrapper').querySelector('code');
|
||||
const codeLines = codeBlock.querySelectorAll('.code-line');
|
||||
let codeText = '';
|
||||
|
||||
codeLines.forEach((line) => {
|
||||
const lineNumber = line.querySelector('.line-number').textContent;
|
||||
const lineContent = line.querySelector('.line-content').textContent;
|
||||
codeText += `${lineNumber} ${lineContent}\n`;
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(codeText.trim()).then(() => {
|
||||
button.classList.add('copied');
|
||||
button.querySelector('svg').style.display = 'none';
|
||||
button.innerHTML = 'Copied!';
|
||||
setTimeout(() => {
|
||||
button.classList.remove('copied');
|
||||
button.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
Copy
|
||||
`;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
async highlightCode(code, language) {
|
||||
// Make sure the language is supported by your highlighting library
|
||||
const supportedLanguage = Prism.languages[language] ? language : 'plaintext';
|
||||
return Prism.highlight(code, Prism.languages[supportedLanguage], supportedLanguage);
|
||||
}
|
||||
|
||||
// Helper function to escape HTML special characters
|
||||
escapeHtml(unsafe) {
|
||||
if (typeof unsafe !== 'string') {
|
||||
console.log("Found unsafe string:", text)
|
||||
return '';
|
||||
}
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 5d805b131b379f0e551023be0ed472f2c23a0245
|
||||
Subproject commit fa70a95766bbda5860af375b0d2c046414066f4a
|
@ -6,7 +6,6 @@
|
||||
<button v-show="!generating" id="generate-button" title="Generate from current cursor position" @click="generate" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><i data-feather="pen-tool"></i></button>
|
||||
<button v-show="!generating" id="generate-next-button" title="Generate from next place holder" @click="generate_in_placeholder" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><i data-feather="archive"></i></button>
|
||||
<button v-show="!generating" id="tokenize" title="Tokenize text" @click="tokenize_text" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><img width="25" height="25" :src="tokenize_icon"></button>
|
||||
|
||||
<span class="w-80"></span>
|
||||
<button v-show="generating" id="stop-button" @click="stopGeneration" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><i data-feather="x"></i></button>
|
||||
<button
|
||||
@ -36,9 +35,21 @@
|
||||
>
|
||||
<img v-if="!pending" :src="is_deaf_transcribing?deaf_on:deaf_off" height="25">
|
||||
<img v-if="pending" :src="loading_icon" height="25">
|
||||
</button>
|
||||
</button>
|
||||
|
||||
<button
|
||||
.slider-value {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
.small-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Start recording audio"
|
||||
@click="startRecording"
|
||||
@ -47,7 +58,6 @@
|
||||
>
|
||||
<img v-if="!pending" :src="is_recording?rec_on:rec_off" height="25">
|
||||
<img v-if="pending" :src="loading_icon" height="25">
|
||||
|
||||
</button>
|
||||
<button v-if="!isSynthesizingVoice"
|
||||
title="generate audio from the text"
|
||||
@ -67,7 +77,7 @@
|
||||
<button v-show="!generating" id="export-button" @click="exportText" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><i data-feather="upload"></i></button>
|
||||
<button v-show="!generating" id="import-button" @click="importText" class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer"><i data-feather="download"></i></button>
|
||||
|
||||
<div class="flex gap-3 flex-1 items-center flex-grow justify-end">
|
||||
<div class="flex gap-3 flex-1 items-center flex-grow justify-end">
|
||||
<button
|
||||
class="border-2 text-blue-600 dark:text-white border-blue-300 p-2 rounded shadow-lg hover:border-gray-600 dark:link-item-dark cursor-pointer"
|
||||
@click="tab_id='source'" :class="{'bg-blue-200 dark:bg-blue-500':tab_id=='source'}">
|
||||
@ -80,7 +90,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="import-input" class="hidden">
|
||||
|
||||
</div>
|
||||
<div class="flex-grow m-2 p-2 border border-blue-300 rounded-md border-2 border-blue-300 m-2 p-4" :class="{ 'border-red-500': generating }">
|
||||
<div v-if="tab_id === 'source'">
|
||||
@ -117,7 +126,6 @@
|
||||
title="Add bash block" @click.stop="addBlock('bash')">
|
||||
<img :src="bash_block" width="25" height="25">
|
||||
</div>
|
||||
|
||||
<div class="text-lg hover:text-secondary duration-75 active:scale-90 p-2 cursor-pointer hover:border-2"
|
||||
title="Copy message to clipboard" @click.stop="copyContentToClipboard()">
|
||||
<i data-feather="copy"></i>
|
||||
@ -133,7 +141,7 @@
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<span>Cursor position {{ cursorPosition }}</span>
|
||||
<span>Cursor position {{ cursorPosition }}</span>
|
||||
</div>
|
||||
<audio controls v-if="audio_url!=null" :key="audio_url">
|
||||
<source :src="audio_url" type="audio/wav" ref="audio_player">
|
||||
@ -141,15 +149,19 @@
|
||||
</audio>
|
||||
<tokens-hilighter :namedTokens="namedTokens">
|
||||
|
||||
</tokens-hilighter>
|
||||
</tokens-hilighter>
|
||||
<div v-if="tab_id === 'render'">
|
||||
<MarkdownRenderer ref="mdRender" :client_id="this.$store.state.client_id" :message_id="0" :discussion_id="0" :markdown-text="text" class="mt-4 p-2 rounded shadow-lg dark:bg-bg-dark">
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card title="settings" class="slider-container ml-0 mr-0" :isHorizontal="false" :disableHoverAnimation="true" :disableFocus="true">
|
||||
<Card title="Model" class="slider-container ml-0 mr-0" :is_subcard="true" :isHorizontal="false" :disableHoverAnimation="true" :disableFocus="true">
|
||||
<div class="settings-button" @click="showSettings = !showSettings">
|
||||
<i data-feather="settings"></i> Settings
|
||||
</div>
|
||||
<div v-if="showSettings" class="settings bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">Settings</h2>
|
||||
<Card title="Model" class="slider-container ml-0 mr-0" :isHorizontal="false" :disableHoverAnimation="true" :disableFocus="true">
|
||||
<select v-model="this.$store.state.selectedModel" @change="setModel" class="bg-white dark:bg-black m-0 border-2 rounded-md shadow-sm w-full">
|
||||
<option v-for="model in models" :key="model" :value="model">
|
||||
{{ model }}
|
||||
@ -171,7 +183,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Card>
|
||||
</Card>
|
||||
<Card title="Presets" class="slider-container ml-0 mr-0" :is_subcard="true" :isHorizontal="false" :disableHoverAnimation="true" :disableFocus="true">
|
||||
<select v-model="selectedPreset" class="bg-white dark:bg-black mb-2 border-2 rounded-md shadow-sm w-full">
|
||||
<option v-for="preset in presets" :key="preset" :value="preset">
|
||||
@ -183,11 +195,10 @@
|
||||
<button class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer" @click="addPreset" title="Add this text as a preset"><i data-feather="plus"></i></button>
|
||||
<button class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer" @click="removePreset" title="Remove preset"><i data-feather="x"></i></button>
|
||||
<button class="w-6 ml-2 hover:text-secondary duration-75 active:scale-90 cursor-pointer" @click="reloadPresets" title="Reload presets list"><i data-feather="refresh-ccw"></i></button>
|
||||
|
||||
</Card>
|
||||
<Card title="Generation params" class="slider-container ml-0 mr-0" :is_subcard="true" :isHorizontal="false" :disableHoverAnimation="true" :disableFocus="true">
|
||||
|
||||
<div class="slider-container ml-2 mr-2">
|
||||
<div class="slider-container ml-2 mr-2">
|
||||
<h3 class="text-gray-600">Temperature</h3>
|
||||
<input type="range" v-model="temperature" min="0" max="5" step="0.1" class="w-full">
|
||||
<span class="slider-value text-gray-500">Current value: {{ temperature }}</span>
|
||||
@ -228,7 +239,7 @@
|
||||
<span class="slider-value text-gray-500">Current value: {{ seed }}</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast ref="toast"/>
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit ea3581eda1c421c761505c6190c8c3041295acd6
|
||||
Subproject commit 67539d31d70b5dff9dd578310c6429538cd2651b
|
Loading…
Reference in New Issue
Block a user