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

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:
Augustin
2026-04-27 21:01:08 +02:00
parent f4af63afec
commit cb525e6598
50 changed files with 11144 additions and 469 deletions

View File

@@ -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={() => {