feat: open links in new tab with security attributes
- Add target="_blank" and rel="noopener noreferrer" to all links - Prevents tabnabbing and improves UX - Updated layout margins (1400px max-width, 10px padding, 95vh height) - Fixed markdown rendering (assistant HTML preserved without <br> conversion)
This commit is contained in:
parent
b7f0a567da
commit
df4f73a36d
3 changed files with 60 additions and 12 deletions
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -301,7 +302,24 @@ func renderMarkdown(input string) string {
|
||||||
logger.ErrorCF("http", "renderMarkdown error", map[string]any{"error": err.Error()})
|
logger.ErrorCF("http", "renderMarkdown error", map[string]any{"error": err.Error()})
|
||||||
return input // fallback to plain text on error
|
return input // fallback to plain text on error
|
||||||
}
|
}
|
||||||
return buf.String()
|
|
||||||
|
// Add target="_blank" to all links for security and UX
|
||||||
|
html := buf.String()
|
||||||
|
|
||||||
|
// Pattern to match <a href="..."> links
|
||||||
|
// This regex adds target="_blank" and rel="noopener noreferrer" to all <a> tags
|
||||||
|
// that don't already have a target attribute
|
||||||
|
re := regexp.MustCompile(`<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>`)
|
||||||
|
html = re.ReplaceAllStringFunc(html, func(match string) string {
|
||||||
|
// Check if target attribute already exists
|
||||||
|
if strings.Contains(match, `target=`) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
// Insert target and rel attributes before closing >
|
||||||
|
return strings.Replace(match, `>`, ` target="_blank" rel="noopener noreferrer">`, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
func min(a, b int) int {
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
background: #0f0f23;
|
background: #0f0f23;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 90vh;
|
height: 95vh;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
white-space: normal; /* Evita que los \n del HTML se rendericen como espacios */
|
||||||
}
|
}
|
||||||
.message.user {
|
.message.user {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
@ -96,6 +97,31 @@
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
/* Estilos para tablas en HTTP */
|
||||||
|
.message.assistant table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.message.assistant th,
|
||||||
|
.message.assistant td {
|
||||||
|
border: 1px solid #475569;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap; /* Evita saltos dentro de celdas */
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
.message.assistant th {
|
||||||
|
background: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.message.assistant tr:nth-child(even) td {
|
||||||
|
background: #162032;
|
||||||
|
}
|
||||||
.message.assistant pre {
|
.message.assistant pre {
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
@ -253,11 +279,15 @@
|
||||||
function addMessage(content, type) {
|
function addMessage(content, type) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `message ${type}`;
|
div.className = `message ${type}`;
|
||||||
// Convert newlines to <br> for proper rendering in HTML
|
let processedContent;
|
||||||
// For user messages, we need to preserve line breaks from textarea
|
if (type === 'user') {
|
||||||
let processedContent = content.replace(/\n/g, '<br>');
|
// For user messages (plain text), convert newlines to <br>
|
||||||
// Also handle double newlines as paragraphs (optional, for better readability)
|
processedContent = content.replace(/\n/g, '<br>');
|
||||||
processedContent = processedContent.replace(/\n\n/g, '<br><br>');
|
processedContent = processedContent.replace(/\n\n/g, '<br><br>');
|
||||||
|
} else {
|
||||||
|
// For assistant messages (HTML from backend), use as-is
|
||||||
|
processedContent = content;
|
||||||
|
}
|
||||||
div.innerHTML = processedContent;
|
div.innerHTML = processedContent;
|
||||||
chatContainer.appendChild(div);
|
chatContainer.appendChild(div);
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,9 @@ func DefaultConfig() *Config {
|
||||||
ReplyTimeout: 5,
|
ReplyTimeout: 5,
|
||||||
},
|
},
|
||||||
HTTP: HTTPConfig{
|
HTTP: HTTPConfig{
|
||||||
Enabled: false,
|
Enabled: true,
|
||||||
Host: "0.0.0.0",
|
Host: "127.0.0.1",
|
||||||
Port: 8080,
|
Port: 8070,
|
||||||
AllowFrom: FlexibleStringSlice{},
|
AllowFrom: FlexibleStringSlice{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue