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:
JaviLib 2026-02-22 05:59:56 +01:00
parent b7f0a567da
commit df4f73a36d
3 changed files with 60 additions and 12 deletions

View file

@ -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 {

View file

@ -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;

View file

@ -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{},
}, },
}, },