fix(ui): add missing copiedMsg state, remove dead code, bump v0.9.3
All checks were successful
Stable Release / stable (push) Successful in 1m58s
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:
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "muyue"
|
Name = "muyue"
|
||||||
Version = "0.9.2"
|
Version = "0.9.3"
|
||||||
Author = "La Légion de Muyue"
|
Author = "La Légion de Muyue"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||||
import { useI18n } from '../i18n'
|
|
||||||
import mermaid from 'mermaid'
|
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
@@ -9,8 +7,6 @@ import rehypeHighlight from 'rehype-highlight'
|
|||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
import 'highlight.js/styles/github-dark.css'
|
import 'highlight.js/styles/github-dark.css'
|
||||||
|
|
||||||
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
|
|
||||||
|
|
||||||
const RANKS = {
|
const RANKS = {
|
||||||
commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' },
|
commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' },
|
||||||
general: { label: 'General', short: 'GEN', color: '#FF9100' },
|
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
|
|
||||||
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 }) {
|
function ThinkingBlock({ content, done, raw }) {
|
||||||
return (
|
return (
|
||||||
<div className={`feed-thinking-block ${done ? 'done' : 'active'}`}>
|
<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 }) {
|
function MarkdownContent({ content, raw }) {
|
||||||
if (raw) {
|
if (raw) {
|
||||||
return <pre className="feed-content" style={{ whiteSpace: 'pre-wrap', fontFamily: 'var(--font-mono)', fontSize: '0.9em' }}>{content}</pre>
|
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 isUser = msg.role === 'user'
|
||||||
const isSystem = msg.role === 'system'
|
const isSystem = msg.role === 'system'
|
||||||
const rank = getRank(msg.role)
|
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' }) : ''
|
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') ? (
|
{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 (
|
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) => {
|
{parsedSegments.map((seg, i) => {
|
||||||
if (seg.type === 'text') {
|
if (seg.type === 'text') {
|
||||||
if (!seg.content) return null
|
if (!seg.content) return null
|
||||||
@@ -406,21 +265,9 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{parsedToolCalls && (() => {
|
{parsedToolCalls && (() => {
|
||||||
const compress = collapseHistory && !forceExpand && parsedToolCalls.length > 1
|
|
||||||
const items = compress ? parsedToolCalls.slice(-1) : parsedToolCalls
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{compress && (
|
{parsedToolCalls.map((tc, i) => {
|
||||||
<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) => {
|
|
||||||
const resultData = parsedToolResults
|
const resultData = parsedToolResults
|
||||||
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
|
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
|
||||||
: null
|
: null
|
||||||
@@ -448,17 +295,6 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
|||||||
const rank = RANKS.general
|
const rank = RANKS.general
|
||||||
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
|
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
|
||||||
const hasToolCalls = toolCalls && toolCalls.length > 0
|
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')
|
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} />}
|
{thinking && <ThinkingBlock content={thinking} raw done={false} />}
|
||||||
{hasOrderedSegments ? (
|
{hasOrderedSegments ? (
|
||||||
<>
|
<>
|
||||||
{compress && (
|
{segments.map((seg, i) => {
|
||||||
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
|
if (seg.type === 'text') {
|
||||||
<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>
|
if (!seg.content) return null
|
||||||
<button
|
return (
|
||||||
type="button"
|
<div key={`t${i}`} className="feed-content">
|
||||||
onClick={() => setForceExpand(true)}
|
<MarkdownContent content={seg.content} raw={false} />
|
||||||
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
|
</div>
|
||||||
>Tout afficher</button>
|
)
|
||||||
</div>
|
}
|
||||||
)}
|
if (seg.type === 'tool') {
|
||||||
{(() => {
|
return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} />
|
||||||
const lastToolId = toolSegments.length > 0 ? toolSegments[toolSegments.length - 1] : null
|
}
|
||||||
return segments.map((seg, i) => {
|
return null
|
||||||
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
|
|
||||||
})
|
|
||||||
})()}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{hasToolCalls && (compress
|
{hasToolCalls && toolCalls.map((tc, i) => (
|
||||||
? [<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) => (
|
|
||||||
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} />
|
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} />
|
||||||
))
|
))
|
||||||
)}
|
}
|
||||||
{cleanContent && (
|
{cleanContent && (
|
||||||
<div className="feed-content">
|
<div className="feed-content">
|
||||||
<MarkdownContent content={cleanContent} raw={false} />
|
<MarkdownContent content={cleanContent} raw={false} />
|
||||||
@@ -551,7 +371,6 @@ export default function Studio({ api }) {
|
|||||||
const [sudoModal, setSudoModal] = useState(null)
|
const [sudoModal, setSudoModal] = useState(null)
|
||||||
const [attachedImages, setAttachedImages] = useState([])
|
const [attachedImages, setAttachedImages] = useState([])
|
||||||
const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 })
|
const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 })
|
||||||
const [toolModes, setToolModes] = useState({})
|
|
||||||
const [advancedReflection, setAdvancedReflection] = useState(() => {
|
const [advancedReflection, setAdvancedReflection] = useState(() => {
|
||||||
try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false }
|
try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user