1- import { useState , useEffect } from 'react' ;
1+ import { useState , useEffect , useCallback } from 'react' ;
22import { Send , User , Bot , Shield , Eye , Terminal , Loader } from 'lucide-react' ;
3+ import ReactMarkdown from 'react-markdown' ;
34import { 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
623interface ChatMessage { role : 'user' | 'assistant' | 'system' ; content : string ; timestamp : string ; }
724
@@ -14,58 +31,61 @@ const TENANT_OPTIONS = [
1431
1532export 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