fix(ui): add missing copiedMsg state, remove dead code, bump v0.9.3
All checks were successful
Stable Release / stable (push) Successful in 1m58s

The copiedMsg state was referenced in the Copy MD button but never declared,
causing a ReferenceError crash at runtime. Also removed unused compress
logic (collapseHistory/forceExpand), dead functions (renderContent, formatText,
CodeBlockWithCopy, MermaidBlock), and unused imports (useMemo, mermaid).

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-28 14:50:55 +02:00
parent 8b8f217624
commit 3f4432d88a
2 changed files with 20 additions and 201 deletions

View File

@@ -1,6 +1,4 @@
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { useI18n } from '../i18n'
import mermaid from 'mermaid'
import { useState, useRef, useEffect, useCallback } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
@@ -9,8 +7,6 @@ import rehypeHighlight from 'rehype-highlight'
import 'katex/dist/katex.min.css'
import 'highlight.js/styles/github-dark.css'
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
const RANKS = {
commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' },
general: { label: 'General', short: 'GEN', color: '#FF9100' },
@@ -40,72 +36,6 @@ function RankIcon({ rank }) {
)
}
function renderContent(text) {
const parts = []
const codeBlockRegex = /(```[\s\S]*?```)/g
let match
let lastIndex = 0
while ((match = codeBlockRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ type: 'text', content: text.slice(lastIndex, match.index) })
}
const full = match[1]
const firstNewline = full.indexOf('\n')
const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : ''
const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3)
parts.push({ type: 'code', lang, content: code })
lastIndex = match.index + full.length
}
if (lastIndex < text.length) {
const remaining = text.slice(lastIndex)
const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/)
if (openBlock) {
if (openBlock.index > 0) {
parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) })
}
parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' })
} else {
parts.push({ type: 'text', content: remaining })
}
}
return parts
}
function formatText(text) {
let html = text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
html = html.replace(/^(\|.+\|)\n(\|[\s\-:|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => {
const headers = headerRow.split('|').filter(c => c.trim() !== '').map(c => `<th>${c.trim()}</th>`).join('')
const rows = bodyRows.trim().split('\n').map(row => {
const cells = row.split('|').filter(c => c.trim() !== '').map(c => `<td>${c.trim()}</td>`).join('')
return `<tr>${cells}</tr>`
}).join('')
return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`
})
html = html
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
.replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
.replace(/^---+$/gm, '<hr>')
.replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">\u2022 $1</div>')
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
.replace(/\n/g, '<br/>')
html = html
.replace(/<br\/>\s*<br\/>/g, '<br/>')
.replace(/<br\/>\s*(<h[234]|<div class="msg-|<table|<hr)/g, '$1')
.replace(/(<\/h[234]|<\/div>|<\/table>|<hr>)\s*<br\/>/g, '$1')
.replace(/\s+on\w+=["'][^"']*["']/gi, '')
.replace(/javascript:/gi, '')
.replace(/data:/gi, '')
return html
}
function ThinkingBlock({ content, done, raw }) {
return (
<div className={`feed-thinking-block ${done ? 'done' : 'active'}`}>
@@ -219,64 +149,6 @@ function ToolCallBlock({ call, result, activeAgents, onModeChange }) {
)
}
let mermaidIdCounter = 0
function MermaidBlock({ code }) {
const ref = useRef(null)
const [svg, setSvg] = useState('')
const [error, setError] = useState(false)
useEffect(() => {
let cancelled = false
const id = `studio-mermaid-${++mermaidIdCounter}`
mermaid.render(id, code).then(({ svg }) => {
if (!cancelled) setSvg(svg)
}).catch(() => {
if (!cancelled) setError(true)
})
return () => { cancelled = true }
}, [code])
if (error) return <pre className="studio-mermaid-error">{code}</pre>
if (!svg) return <div className="studio-mermaid-loading">Chargement...</div>
return <div className="studio-mermaid-container" ref={ref} dangerouslySetInnerHTML={{ __html: svg }} />
}
function CodeBlockWithCopy({ part, index, copiedIdx, setCopiedIdx }) {
if (part.lang === 'mermaid') {
return (
<div className="studio-code-block">
<div className="studio-code-header">
<span className="studio-code-lang">mermaid</span>
<button className={`studio-copy-btn ${copiedIdx === index ? 'copied' : ''}`} onClick={() => {
navigator.clipboard.writeText(part.content)
setCopiedIdx(index)
setTimeout(() => setCopiedIdx(null), 1500)
}}>
{copiedIdx === index ? 'Copie!' : 'Copier'}
</button>
</div>
<MermaidBlock code={part.content} />
</div>
)
}
return (
<div className="studio-code-block">
<div className="studio-code-header">
{part.lang && <span className="studio-code-lang">{part.lang}</span>}
<button className={`studio-copy-btn ${copiedIdx === index ? 'copied' : ''}`} onClick={() => {
navigator.clipboard.writeText(part.content)
setCopiedIdx(index)
setTimeout(() => setCopiedIdx(null), 1500)
}}>
{copiedIdx === index ? 'Copie!' : 'Copier'}
</button>
</div>
<pre><code>{part.content}</code></pre>
</div>
)
}
function MarkdownContent({ content, raw }) {
if (raw) {
return <pre className="feed-content" style={{ whiteSpace: 'pre-wrap', fontFamily: 'var(--font-mono)', fontSize: '0.9em' }}>{content}</pre>
@@ -292,7 +164,7 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
const isUser = msg.role === 'user'
const isSystem = msg.role === 'system'
const rank = getRank(msg.role)
const [copiedIdx, setCopiedIdx] = useState(null)
const [copiedMsg, setCopiedMsg] = useState(false)
const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
@@ -365,21 +237,8 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
)}
{parsedSegments && parsedSegments.some(s => s.type === 'tool') ? (
(() => {
const toolSegs = parsedSegments.filter(s => s.type === 'tool')
const compress = collapseHistory && !forceExpand && toolSegs.length > 1
const lastTool = toolSegs.length > 0 ? toolSegs[toolSegs.length - 1] : null
return (
<>
{compress && (
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {toolSegs.length - 1} action{toolSegs.length - 1 > 1 ? 's' : ''} précédente{toolSegs.length - 1 > 1 ? 's' : ''} masquée{toolSegs.length - 1 > 1 ? 's' : ''}</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{parsedSegments.map((seg, i) => {
if (seg.type === 'text') {
if (!seg.content) return null
@@ -406,21 +265,9 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
) : (
<>
{parsedToolCalls && (() => {
const compress = collapseHistory && !forceExpand && parsedToolCalls.length > 1
const items = compress ? parsedToolCalls.slice(-1) : parsedToolCalls
return (
<>
{compress && (
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {parsedToolCalls.length - 1} action{parsedToolCalls.length - 1 > 1 ? 's' : ''} précédente{parsedToolCalls.length - 1 > 1 ? 's' : ''} masquée{parsedToolCalls.length - 1 > 1 ? 's' : ''}</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{items.map((tc, i) => {
{parsedToolCalls.map((tc, i) => {
const resultData = parsedToolResults
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
: null
@@ -448,17 +295,6 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
const rank = RANKS.general
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
const hasToolCalls = toolCalls && toolCalls.length > 0
const [copiedIdx, setCopiedIdx] = useState(null)
const renderedContent = useMemo(() => {
if (!cleanContent) return null
return null
}, [cleanContent])
const formattedThinking = useMemo(() => {
if (!thinking) return ''
return thinking
}, [thinking])
const hasOrderedSegments = segments && segments.some(s => s.type === 'tool')
@@ -477,43 +313,27 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
{thinking && <ThinkingBlock content={thinking} raw done={false} />}
{hasOrderedSegments ? (
<>
{compress && (
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {toolSegments.length - 1} action{toolSegments.length - 1 > 1 ? 's' : ''} précédente{toolSegments.length - 1 > 1 ? 's' : ''} masquée{toolSegments.length - 1 > 1 ? 's' : ''} (mode compressé)</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{(() => {
const lastToolId = toolSegments.length > 0 ? toolSegments[toolSegments.length - 1] : null
return segments.map((seg, i) => {
if (seg.type === 'text') {
if (!seg.content) return null
return (
<div key={`t${i}`} className="feed-content">
<MarkdownContent content={seg.content} raw={false} />
</div>
)
}
if (seg.type === 'tool') {
if (compress && seg !== lastToolId) return null
return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} />
}
return null
})
})()}
{segments.map((seg, i) => {
if (seg.type === 'text') {
if (!seg.content) return null
return (
<div key={`t${i}`} className="feed-content">
<MarkdownContent content={seg.content} raw={false} />
</div>
)
}
if (seg.type === 'tool') {
return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} />
}
return null
})}
</>
) : (
<>
{hasToolCalls && (compress
? [<ToolCallBlock key={toolCalls[toolCalls.length - 1].call?.tool_call_id || 'last'} call={toolCalls[toolCalls.length - 1].call} result={toolCalls[toolCalls.length - 1].result} activeAgents={activeAgents} onModeChange={onModeChange} />]
: toolCalls.map((tc, i) => (
{hasToolCalls && toolCalls.map((tc, i) => (
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} />
))
)}
}
{cleanContent && (
<div className="feed-content">
<MarkdownContent content={cleanContent} raw={false} />
@@ -551,7 +371,6 @@ export default function Studio({ api }) {
const [sudoModal, setSudoModal] = useState(null)
const [attachedImages, setAttachedImages] = useState([])
const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 })
const [toolModes, setToolModes] = useState({})
const [advancedReflection, setAdvancedReflection] = useState(() => {
try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false }
})