Compare commits
2 Commits
v0.3.3-bet
...
v0.3.3-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1074b019d3 | ||
|
|
2da0cf9421 |
@@ -53,11 +53,9 @@ function renderContent(text) {
|
||||
}
|
||||
|
||||
function formatText(text) {
|
||||
// First escape HTML entities
|
||||
let html = text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
// Apply markdown transformations (now with escaped brackets)
|
||||
html = html
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||
@@ -66,10 +64,10 @@ function formatText(text) {
|
||||
.replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
|
||||
.replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">• $1</div>')
|
||||
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
|
||||
.replace(/\n/g, '<br/>')
|
||||
|
||||
// Sanitize: remove event handlers and dangerous protocols
|
||||
html = html
|
||||
.replace(/\s+on\w+=["'][^"']*["']/gi, '') // Remove on* event handlers
|
||||
.replace(/\s+on\w+=["'][^"']*["']/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/data:/gi, '')
|
||||
|
||||
@@ -331,6 +329,20 @@ export default function Studio({ api }) {
|
||||
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages, streaming, streamThinking, streamToolCalls])
|
||||
|
||||
useEffect(() => {
|
||||
const onTab = (e) => {
|
||||
if (e.key !== 'Tab') return
|
||||
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return
|
||||
const feed = document.querySelector('.studio-feed-layout')
|
||||
if (!feed?.closest('.tab-hidden')) {
|
||||
e.preventDefault()
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onTab)
|
||||
return () => window.removeEventListener('keydown', onTab)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
@@ -539,10 +551,38 @@ export default function Studio({ api }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const COMMANDS = ['/clear', '/summarize', '/help', '/plan', '/export', '/model']
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
return
|
||||
}
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const ta = textareaRef.current
|
||||
if (!ta) return
|
||||
if (document.activeElement !== ta) {
|
||||
ta.focus()
|
||||
return
|
||||
}
|
||||
const val = ta.value
|
||||
const pos = ta.selectionStart
|
||||
const before = val.slice(0, pos)
|
||||
const afterSlash = before.match(/\/(\w*)$/)
|
||||
if (afterSlash) {
|
||||
const partial = afterSlash[0]
|
||||
const matches = COMMANDS.filter(c => c.startsWith(partial) && c !== partial)
|
||||
if (matches.length === 1) {
|
||||
const completed = matches[0] + ' '
|
||||
const newText = val.slice(0, pos - afterSlash[0].length) + completed + val.slice(pos)
|
||||
setInput(newText)
|
||||
requestAnimationFrame(() => {
|
||||
ta.selectionStart = ta.selectionEnd = pos - afterSlash[0].length + completed.length
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user