feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
All checks were successful
Beta Release / beta (push) Successful in 5m9s
All checks were successful
Beta Release / beta (push) Successful in 5m9s
Major additions: - RAG pipeline (indexing, chunking, search) with sidebar upload button - Memory system with CRUD API - Plugins and lessons modules - MCP discovery and MCP server - Advanced skills (auto-create, conditional, improver) - Agent browser/image support, delegate, sessions - File editor with CodeMirror in split panes - Markdown rendering via react-markdown + KaTeX + highlight.js - Raw markdown toggle - PWA manifest + service worker - Extension UI redesign with new design tokens and studio-style chat - Pipeline API for chat streaming - Mobile responsive layout 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
import mermaid from 'mermaid'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
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)' })
|
||||
|
||||
@@ -279,6 +286,14 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
|
||||
|
||||
const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
|
||||
|
||||
const [showRawMarkdown, setShowRawMarkdown] = useState(() => {
|
||||
try { return localStorage.getItem('muyue.showRawMarkdown') === 'true' } catch { return false }
|
||||
})
|
||||
|
||||
const renderMarkdown = useCallback((content) => {
|
||||
return <MarkdownContent content={content} raw={showRawMarkdown} />
|
||||
}, [showRawMarkdown])
|
||||
|
||||
let parsedToolCalls = null
|
||||
let parsedToolResults = null
|
||||
let parsedSegments = null
|
||||
@@ -322,7 +337,7 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
|
||||
<span className="feed-role">{rank.label}</span>
|
||||
{timeStr && <span className="feed-time">{timeStr}</span>}
|
||||
</div>
|
||||
{msg.thinking && <ThinkingBlock content={formatText(msg.thinking)} done raw />}
|
||||
{msg.thinking && <ThinkingBlock content={msg.thinking} done raw />}
|
||||
{msg.images && msg.images.length > 0 && (
|
||||
<div className="feed-images">
|
||||
{msg.images.map((imgId, i) => (
|
||||
@@ -354,13 +369,7 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
|
||||
if (!c) return null
|
||||
return (
|
||||
<div key={`t${i}`} className="feed-content">
|
||||
{renderContent(c).map((part, j) =>
|
||||
part.type === 'code' ? (
|
||||
<CodeBlockWithCopy key={j} part={part} index={j} copiedIdx={copiedIdx} setCopiedIdx={setCopiedIdx} />
|
||||
) : (
|
||||
<span key={j} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
||||
)
|
||||
)}
|
||||
{renderMarkdown(c)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -408,13 +417,7 @@ function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) {
|
||||
})()}
|
||||
{cleanContent && (
|
||||
<div className="feed-content">
|
||||
{renderContent(cleanContent).map((part, i) =>
|
||||
part.type === 'code' ? (
|
||||
<CodeBlockWithCopy key={i} part={part} index={i} copiedIdx={copiedIdx} setCopiedIdx={setCopiedIdx} />
|
||||
) : (
|
||||
<span key={i} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
||||
)
|
||||
)}
|
||||
{renderMarkdown(cleanContent)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -432,13 +435,13 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
const [forceExpand, setForceExpand] = useState(false)
|
||||
|
||||
const renderedContent = useMemo(() => {
|
||||
if (!cleanContent) return []
|
||||
return renderContent(cleanContent)
|
||||
if (!cleanContent) return null
|
||||
return null
|
||||
}, [cleanContent])
|
||||
|
||||
const formattedThinking = useMemo(() => {
|
||||
if (!thinking) return ''
|
||||
return formatText(thinking)
|
||||
return thinking
|
||||
}, [thinking])
|
||||
|
||||
const hasOrderedSegments = segments && segments.some(s => s.type === 'tool')
|
||||
@@ -457,7 +460,7 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
</span>
|
||||
<span className="feed-role">{rank.label}</span>
|
||||
</div>
|
||||
{thinking && <ThinkingBlock content={formattedThinking} raw done={false} />}
|
||||
{thinking && <ThinkingBlock content={thinking} raw done={false} />}
|
||||
{hasOrderedSegments ? (
|
||||
<>
|
||||
{compress && (
|
||||
@@ -475,16 +478,9 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
return segments.map((seg, i) => {
|
||||
if (seg.type === 'text') {
|
||||
if (!seg.content) return null
|
||||
const parts = renderContent(seg.content)
|
||||
return (
|
||||
<div key={`t${i}`} className="feed-content">
|
||||
{parts.map((part, j) =>
|
||||
part.type === 'code' ? (
|
||||
<CodeBlockWithCopy key={j} part={part} index={j} copiedIdx={copiedIdx} setCopiedIdx={setCopiedIdx} />
|
||||
) : (
|
||||
<span key={j} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
||||
)
|
||||
)}
|
||||
<MarkdownContent content={seg.content} raw={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -506,13 +502,7 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
)}
|
||||
{cleanContent && (
|
||||
<div className="feed-content">
|
||||
{renderedContent.map((part, i) =>
|
||||
part.type === 'code' ? (
|
||||
<CodeBlockWithCopy key={i} part={part} index={i} copiedIdx={copiedIdx} setCopiedIdx={setCopiedIdx} />
|
||||
) : (
|
||||
<span key={i} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
|
||||
)
|
||||
)}
|
||||
<MarkdownContent content={cleanContent} raw={false} />
|
||||
<span className="studio-cursor" />
|
||||
</div>
|
||||
)}
|
||||
@@ -561,6 +551,8 @@ export default function Studio({ api }) {
|
||||
const textareaRef = useRef(null)
|
||||
const abortRef = useRef(null)
|
||||
const fileInputRef = useRef(null)
|
||||
const ragFileRef = useRef(null)
|
||||
const [ragStatus, setRagStatus] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.getChatHistory().then(data => {
|
||||
@@ -589,6 +581,10 @@ export default function Studio({ api }) {
|
||||
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages, streaming, streamThinking, streamToolCalls])
|
||||
|
||||
useEffect(() => {
|
||||
api.ragStatus().then(setRagStatus).catch(() => {})
|
||||
}, [api])
|
||||
|
||||
useEffect(() => {
|
||||
const onTab = (e) => {
|
||||
if (e.key !== 'Tab') return
|
||||
@@ -671,6 +667,20 @@ export default function Studio({ api }) {
|
||||
setAttachedImages(prev => prev.filter((_, i) => i !== index))
|
||||
}, [])
|
||||
|
||||
const handleRAGFileSelect = useCallback(async (e) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length === 0) return
|
||||
for (const file of files) {
|
||||
try {
|
||||
await api.ragIndexFile(file)
|
||||
} catch (err) {
|
||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'system', content: `RAG: erreur d'indexation de ${file.name}: ${err.message}`, time: new Date().toISOString() }])
|
||||
}
|
||||
}
|
||||
api.ragStatus().then(setRagStatus).catch(() => {})
|
||||
e.target.value = ''
|
||||
}, [api])
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!input.trim() || loading) return
|
||||
const text = input.trim()
|
||||
@@ -1054,6 +1064,14 @@ export default function Studio({ api }) {
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImageSelect}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={ragFileRef}
|
||||
accept=".txt,.md,.go,.js,.ts,.py,.java,.rs,.jsx,.tsx,.json,.yaml,.yml,.csv,.html,.css,.sh,.bash,.zsh,.fish"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleRAGFileSelect}
|
||||
/>
|
||||
<button
|
||||
className="studio-attach-btn"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
@@ -1064,6 +1082,17 @@ export default function Studio({ api }) {
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="studio-attach-btn"
|
||||
onClick={() => ragFileRef.current?.click()}
|
||||
disabled={loading}
|
||||
title={ragStatus ? `RAG: ${ragStatus.documents || 0} docs, ${ragStatus.chunks || 0} chunks` : 'Ajouter un contexte RAG'}
|
||||
style={ragStatus && ragStatus.documents > 0 ? { color: 'var(--accent, #6c5ce7)', borderColor: 'var(--accent, #6c5ce7)' } : undefined}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="studio-attach-btn"
|
||||
onClick={() => {
|
||||
@@ -1079,6 +1108,21 @@ export default function Studio({ api }) {
|
||||
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="studio-attach-btn"
|
||||
onClick={() => {
|
||||
const next = !showRawMarkdown
|
||||
setShowRawMarkdown(next)
|
||||
try { localStorage.setItem('muyue.showRawMarkdown', String(next)) } catch {}
|
||||
}}
|
||||
disabled={loading}
|
||||
title={showRawMarkdown ? "Markdown brut: ON" : "Markdown rendu"}
|
||||
style={showRawMarkdown ? { color: 'var(--accent, #6c5ce7)', borderColor: 'var(--accent, #6c5ce7)' } : undefined}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="studio-attach-btn"
|
||||
onClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user