Skip to content

Commit 8298bf4

Browse files
author
JiaDe
committed
fix: Playground — markdown rendering, chat persistence, cold start retry
1. Add ReactMarkdown for assistant messages (was showing raw ** marks) 2. Chat history persisted to localStorage per tenant (survives navigation) 3. Cold start retry rewritten with async/await (was broken with mutate inside onError callback — sendMut.isPending blocked the retry) 4. 5s retry delay for live mode cold start
1 parent 3875be6 commit 8298bf4

1 file changed

Lines changed: 64 additions & 38 deletions

File tree

enterprise/admin-console/src/pages/Playground.tsx

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22
import { Send, User, Bot, Shield, Eye, Terminal, Loader } from 'lucide-react';
3+
import ReactMarkdown from 'react-markdown';
34
import { Card, Badge, Button, PageHeader, Select } from '../components/ui';
4-
import { usePlaygroundProfiles, usePlaygroundSend } from '../hooks/useApi';
5+
import { usePlaygroundProfiles } from '../hooks/useApi';
6+
import { api } from '../api/client';
7+
8+
interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: string; }
9+
10+
const STORAGE_KEY = 'openclaw_playground_chat';
11+
12+
function loadMessages(tenantId: string): ChatMessage[] {
13+
try {
14+
const raw = localStorage.getItem(`${STORAGE_KEY}_${tenantId}`);
15+
return raw ? JSON.parse(raw) : [];
16+
} catch { return []; }
17+
}
18+
19+
function saveMessages(tenantId: string, messages: ChatMessage[]) {
20+
localStorage.setItem(`${STORAGE_KEY}_${tenantId}`, JSON.stringify(messages));
21+
}
522

623
interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: string; }
724

@@ -14,58 +31,61 @@ const TENANT_OPTIONS = [
1431

1532
export default function Playground() {
1633
const { data: profiles } = usePlaygroundProfiles();
17-
const sendMut = usePlaygroundSend();
1834
const [tenantId, setTenantId] = useState(TENANT_OPTIONS[0].value);
19-
const [messages, setMessages] = useState<ChatMessage[]>([]);
35+
const [messages, setMessages] = useState<ChatMessage[]>(() => loadMessages(TENANT_OPTIONS[0].value));
2036
const [inputValue, setInputValue] = useState('');
2137
const [lastPlanE, setLastPlanE] = useState('No messages processed yet');
2238
const [mode, setMode] = useState<'simulate' | 'live'>('simulate');
39+
const [sending, setSending] = useState(false);
2340

2441
const profile = profiles?.[tenantId] || { role: 'loading', tools: [], planA: '', planE: '' };
2542
const profileLoaded = !!profiles?.[tenantId];
2643

44+
// Persist messages
45+
useEffect(() => { saveMessages(tenantId, messages); }, [messages, tenantId]);
46+
2747
useEffect(() => {
2848
if (!profileLoaded) return;
29-
setMessages([{ role: 'system', content: `🔒 Tenant context loaded: ${profile.role} role, ${profile.tools.length} tools`, timestamp: '' }]);
49+
const saved = loadMessages(tenantId);
50+
if (saved.length > 0) {
51+
setMessages(saved);
52+
} else {
53+
setMessages([{ role: 'system', content: `🔒 Tenant context loaded: ${profile.role} role, ${profile.tools.length} tools`, timestamp: '' }]);
54+
}
3055
setLastPlanE('No messages processed yet');
3156
}, [tenantId, profileLoaded]);
3257

33-
const handleSend = () => {
34-
if (!inputValue.trim()) return;
58+
const handleSend = async () => {
59+
if (!inputValue.trim() || sending) return;
3560
const now = new Date().toLocaleTimeString();
3661
const msg = inputValue.trim();
3762
const userMsg: ChatMessage = { role: 'user', content: msg, timestamp: now };
3863
setMessages(prev => [...prev, userMsg]);
3964
setInputValue('');
65+
setSending(true);
4066

41-
sendMut.mutate({ tenant_id: tenantId, message: msg, mode }, {
42-
onSuccess: (data) => {
43-
const assistantMsg: ChatMessage = { role: 'assistant', content: data.response, timestamp: new Date().toLocaleTimeString() };
44-
setMessages(prev => [...prev, assistantMsg]);
45-
setLastPlanE(data.plan_e);
46-
},
47-
onError: (err) => {
48-
const errMsg = mode === 'live'
49-
? '⏳ Agent is warming up (cold start ~25s). Retrying...'
50-
: '⚠️ Error communicating with agent';
51-
setMessages(prev => [...prev, { role: 'assistant', content: errMsg, timestamp: new Date().toLocaleTimeString() }]);
52-
// Auto-retry once for live mode (cold start)
53-
if (mode === 'live') {
54-
setTimeout(() => {
55-
sendMut.mutate({ tenant_id: tenantId, message: msg, mode }, {
56-
onSuccess: (data) => {
57-
const retryMsg: ChatMessage = { role: 'assistant', content: data.response, timestamp: new Date().toLocaleTimeString() };
58-
setMessages(prev => [...prev, retryMsg]);
59-
setLastPlanE(data.plan_e);
60-
},
61-
onError: () => {
62-
setMessages(prev => [...prev, { role: 'assistant', content: 'Agent is still starting up. Please try again in ~30 seconds.', timestamp: new Date().toLocaleTimeString() }]);
63-
},
64-
});
65-
}, 3000);
67+
try {
68+
const data = await api.post<{ response: string; plan_e: string }>('/playground/send', { tenant_id: tenantId, message: msg, mode });
69+
setMessages(prev => [...prev, { role: 'assistant', content: data.response, timestamp: new Date().toLocaleTimeString() }]);
70+
setLastPlanE(data.plan_e);
71+
} catch (e) {
72+
if (mode === 'live') {
73+
setMessages(prev => [...prev, { role: 'assistant', content: '⏳ Agent is warming up (cold start ~25s). Retrying...', timestamp: new Date().toLocaleTimeString() }]);
74+
// Retry after 5s
75+
try {
76+
await new Promise(r => setTimeout(r, 5000));
77+
const retry = await api.post<{ response: string; plan_e: string }>('/playground/send', { tenant_id: tenantId, message: msg, mode });
78+
setMessages(prev => [...prev, { role: 'assistant', content: retry.response, timestamp: new Date().toLocaleTimeString() }]);
79+
setLastPlanE(retry.plan_e);
80+
} catch {
81+
setMessages(prev => [...prev, { role: 'assistant', content: 'Agent is still starting up. Please try again in ~30 seconds.', timestamp: new Date().toLocaleTimeString() }]);
6682
}
67-
},
68-
});
83+
} else {
84+
setMessages(prev => [...prev, { role: 'assistant', content: '⚠️ Error communicating with agent', timestamp: new Date().toLocaleTimeString() }]);
85+
}
86+
} finally {
87+
setSending(false);
88+
}
6989
};
7090

7191
return (
@@ -98,11 +118,17 @@ export default function Playground() {
98118
{msg.timestamp && ` · ${msg.timestamp}`}
99119
</span>
100120
</div>
101-
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
121+
{msg.role === 'assistant' ? (
122+
<div className="text-sm prose prose-invert prose-sm max-w-none [&_p]:my-1 [&_h1]:text-base [&_h1]:font-bold [&_h2]:text-sm [&_h2]:font-semibold [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0.5 [&_code]:bg-dark-bg [&_code]:px-1 [&_code]:rounded [&_pre]:bg-dark-bg [&_pre]:p-3 [&_pre]:rounded-lg [&_strong]:text-text-primary">
123+
<ReactMarkdown>{msg.content}</ReactMarkdown>
124+
</div>
125+
) : (
126+
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
127+
)}
102128
</div>
103129
</div>
104130
))}
105-
{sendMut.isPending && (
131+
{sending && (
106132
<div className="flex justify-start">
107133
<div className="rounded-lg bg-dark-card border border-dark-border px-3 py-2">
108134
<Loader size={14} className="animate-spin text-primary" />
@@ -113,10 +139,10 @@ export default function Playground() {
113139

114140
<div className="flex gap-2">
115141
<input value={inputValue} onChange={e => setInputValue(e.target.value)}
116-
onKeyDown={e => { if (e.key === 'Enter' && !sendMut.isPending) handleSend(); }}
142+
onKeyDown={e => { if (e.key === 'Enter' && !sending) handleSend(); }}
117143
placeholder="Type a message (try 'run shell command')..."
118144
className="flex-1 rounded-lg border border-dark-border bg-dark-bg px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-primary focus:outline-none" />
119-
<Button variant="primary" onClick={handleSend} disabled={sendMut.isPending}><Send size={16} /></Button>
145+
<Button variant="primary" onClick={handleSend} disabled={sending}><Send size={16} /></Button>
120146
</div>
121147
</Card>
122148

0 commit comments

Comments
 (0)