@@ -2,12 +2,18 @@ import React, { useEffect } from 'react';
2
2
import Button from '@mui/material/Button' ;
3
3
import DelIcon from '@mui/icons-material/Delete' ;
4
4
import { createDockerDesktopClient } from '@docker/extension-api-client' ;
5
- import { IconButton , Link , List , ListItem , ListItemButton , ListItemText , Paper , Stack , TextField , Typography } from '@mui/material' ;
5
+ import { Chip , IconButton , Link , List , ListItem , ListItemButton , ListItemText , Paper , Stack , TextField , Typography } from '@mui/material' ;
6
6
import { getRunArgs } from './args' ;
7
7
import Convert from 'ansi-to-html' ;
8
8
9
9
const convert = new Convert ( { newline : true } ) ;
10
10
11
+ type RPCMessage = {
12
+ jsonrpc ?: string ;
13
+ method : string ;
14
+ params : any ;
15
+ }
16
+
11
17
// Note: This line relies on Docker Desktop's presence as a host application.
12
18
// If you're running this React app in a browser, it won't work properly.
13
19
const client = createDockerDesktopClient ( ) ;
@@ -33,10 +39,11 @@ export function App() {
33
39
34
40
const [ promptInput , setPromptInput ] = React . useState < string > ( '' ) ;
35
41
36
- const [ runOut , setRunOut ] = React . useState < string > ( '' ) ;
42
+ const [ runOut , setRunOut ] = React . useState < RPCMessage [ ] > ( [ ] ) ;
37
43
38
44
const scrollRef = React . useRef < HTMLDivElement > ( null ) ;
39
45
46
+ const [ showDebug , setShowDebug ] = React . useState ( false ) ;
40
47
41
48
useEffect ( ( ) => {
42
49
localStorage . setItem ( 'projects' , JSON . stringify ( projects ) ) ;
@@ -90,12 +97,38 @@ export function App() {
90
97
const delim = client . host . platform === 'win32' ? '\\' : '/' ;
91
98
92
99
const startPrompt = async ( ) => {
93
- let output = ""
94
- const updateOutput = ( data : string ) => {
95
- output += data ;
100
+ let output : RPCMessage [ ] = [ ]
101
+ const updateOutput = ( line : RPCMessage ) => {
102
+ if ( line . method === 'functions' ) {
103
+ const functions = line . params ;
104
+ for ( const func of functions ) {
105
+ const functionId = func . id ;
106
+ const existingFunction = output . find ( o =>
107
+ o . method === 'functions'
108
+ &&
109
+ o . params . find ( ( p : { id : string } ) => p . id === functionId )
110
+ ) ;
111
+ if ( existingFunction ) {
112
+ const existingFunctionParamsIndex = existingFunction . params . findIndex ( ( p : { id : string } ) => p . id === functionId ) ;
113
+ existingFunction . params [ existingFunctionParamsIndex ] = { ...existingFunction . params [ existingFunctionParamsIndex ] , ...func } ;
114
+ output = output . map (
115
+ o => o . method === 'functions'
116
+ ?
117
+ { ...o , params : o . params . map ( ( p : { id : string } ) => p . id === functionId ? { ...p , ...func } : p ) }
118
+ :
119
+ o
120
+ ) ;
121
+ } else {
122
+ output = [ ...output , line ] ;
123
+ }
124
+ }
125
+ }
126
+ else {
127
+ output = [ ...output , line ] ;
128
+ }
96
129
setRunOut ( output ) ;
97
130
}
98
- updateOutput ( " Pulling images\n" )
131
+ updateOutput ( { method : 'message' , params : { debug : ' Pulling images' } } )
99
132
try {
100
133
const pullWriteFiles = await client . docker . cli . exec ( "pull" , [ "vonwig/function_write_files" ] ) ;
101
134
const pullPrompts = await client . docker . cli . exec ( "pull" , [ "vonwig/prompts" ] ) ;
@@ -106,12 +139,12 @@ export function App() {
106
139
"vonwig/function_write_files" ,
107
140
`'` + JSON . stringify ( { files : [ { path : ".openai-api-key" , content : openAIKey , executable : false } ] } ) + `'`
108
141
] ) ;
109
- updateOutput ( JSON . stringify ( { pullWriteFiles, pullPrompts, writeKey } ) ) ;
142
+ updateOutput ( { method : 'message' , params : { debug : JSON . stringify ( { pullWriteFiles, pullPrompts, writeKey } ) } } ) ;
110
143
}
111
144
catch ( e ) {
112
- updateOutput ( JSON . stringify ( e ) ) ;
145
+ updateOutput ( { method : 'message' , params : { debug : JSON . stringify ( e ) } } ) ;
113
146
}
114
- updateOutput ( " Running prompts\n" )
147
+ updateOutput ( { method : 'message' , params : { debug : ' Running prompts...' } } )
115
148
const args = getRunArgs ( selectedPrompt ! , selectedProject ! , "" , client . host . platform )
116
149
117
150
client . docker . cli . exec ( "run" , args , {
@@ -120,24 +153,33 @@ export function App() {
120
153
onOutput : ( { stdout, stderr } ) => {
121
154
if ( stdout && stdout . startsWith ( '{' ) ) {
122
155
let rpcMessage = stdout . split ( '}Content-Length:' ) [ 0 ]
123
- if ( ! rpcMessage . endsWith ( '}' ) ) {
156
+ if ( ! rpcMessage . endsWith ( '}} ' ) ) {
124
157
rpcMessage += '}'
125
158
}
126
159
const json = JSON . parse ( rpcMessage )
127
- if ( json . params . content ) {
128
- output += json . params . content
129
- }
160
+ updateOutput ( json )
161
+ // {
162
+ // "jsonrpc": "2.0",
163
+ // "method": "functions",
164
+ // "params": [
165
+ // {
166
+ // "function": {
167
+ // "name": "run-eslint",
168
+ // "arguments": "{\n \""
169
+ // },
170
+ // "id": "call_53E2o4fq1QEmIHixWcKZmOqo"
171
+ // }
172
+ // ]
173
+ // }
130
174
}
131
175
if ( stderr ) {
132
- output += stderr
176
+ updateOutput ( { method : 'message' , params : { debug : stderr } } ) ;
133
177
}
134
- setRunOut ( output ) ;
135
178
} ,
136
179
onError : ( err ) => {
137
180
console . error ( err ) ;
138
- output += err ;
139
- setRunOut ( output ) ;
140
- }
181
+ updateOutput ( { method : 'message' , params : { debug : err } } ) ;
182
+ } ,
141
183
}
142
184
} ) ;
143
185
}
@@ -196,18 +238,31 @@ export function App() {
196
238
{ /* Prompts column */ }
197
239
< Paper sx = { { padding : 1 } } >
198
240
< Typography variant = "h3" > Prompts</ Typography >
199
- < TextField
200
- sx = { { width : '100%' , mt : 1 } }
201
- placeholder = 'Enter GitHub ref or URL'
202
- value = { promptInput }
203
- onChange = { ( e ) => setPromptInput ( e . target . value ) }
204
- />
205
- { promptInput . length > 0 && (
241
+ < Stack direction = 'row' spacing = { 1 } alignItems = { 'center' } justifyContent = { 'space-between' } >
242
+ < TextField
243
+ fullWidth
244
+ placeholder = 'Enter GitHub ref or URL'
245
+ value = { promptInput }
246
+ onChange = { ( e ) => setPromptInput ( e . target . value ) }
247
+ />
248
+ { promptInput . length > 0 && (
249
+ < Button onClick = { ( ) => {
250
+ setPrompts ( [ ...prompts , promptInput ] ) ;
251
+ setPromptInput ( '' ) ;
252
+ } } > Add prompt</ Button >
253
+ ) }
206
254
< Button onClick = { ( ) => {
207
- setPrompts ( [ ...prompts , promptInput ] ) ;
208
- setPromptInput ( '' ) ;
209
- } } > Add prompt</ Button >
210
- ) }
255
+ client . desktopUI . dialog . showOpenDialog ( {
256
+ properties : [ 'openDirectory' , 'multiSelections' ]
257
+ } ) . then ( ( result ) => {
258
+ if ( result . canceled ) {
259
+ return ;
260
+ }
261
+ setPrompts ( [ ...prompts , ...result . filePaths . map ( p => `local://${ p } ` ) ] ) ;
262
+ } ) ;
263
+ } } > Add local prompt</ Button >
264
+ </ Stack >
265
+
211
266
< List >
212
267
{ prompts . map ( ( prompt ) => (
213
268
< ListItem
@@ -232,8 +287,12 @@ export function App() {
232
287
} >
233
288
< ListItemButton sx = { { padding : 0 , pl : 1.5 } } onClick = { ( ) => {
234
289
setSelectedPrompt ( prompt ) ;
235
- } } >
236
- < ListItemText primary = { prompt . split ( delim ) . pop ( ) } secondary = { prompt } />
290
+ } } > {
291
+ prompt . startsWith ( 'local://' ) ?
292
+ < > < ListItemText primary = { < > { prompt . split ( delim ) . pop ( ) } < Chip sx = { { ml : 1 } } label = 'local' /> </ > } secondary = { prompt . replace ( 'local://' , '' ) } /> </ >
293
+ :
294
+ < ListItemText primary = { prompt . split ( '/' ) . pop ( ) } secondary = { prompt } />
295
+ }
237
296
</ ListItemButton >
238
297
</ ListItem >
239
298
) ) }
@@ -259,10 +318,30 @@ export function App() {
259
318
) }
260
319
{ /* Show run output */ }
261
320
{
262
- runOut && (
321
+ runOut . length > 0 && (
263
322
< Paper sx = { { p : 1 } } >
264
- < Typography variant = 'h3' > Run output</ Typography >
265
- < div style = { { whiteSpace : 'pre-wrap' } } dangerouslySetInnerHTML = { { __html : convert . toHtml ( runOut ) } } />
323
+ < Stack direction = 'row' spacing = { 1 } alignItems = { 'center' } justifyContent = { 'space-between' } >
324
+ < Typography variant = 'h3' > Run output</ Typography >
325
+ < Button onClick = { ( ) => setShowDebug ( ! showDebug ) } > { showDebug ? 'Hide' : 'Show' } debug</ Button >
326
+ </ Stack >
327
+
328
+ < div style = { { overflow : 'auto' , maxHeight : '100vh' } } >
329
+ { runOut . map ( ( line , i ) => {
330
+ if ( line . method === 'message' ) {
331
+ if ( line . params . debug ) {
332
+ return showDebug ? < Typography key = { i } variant = 'body1' sx = { theme => ( { color : theme . palette . docker . grey [ 400 ] } ) } > { line . params . debug } </ Typography > : null ;
333
+ }
334
+ if ( line . params . role === 'assistant' ) {
335
+ return < Typography key = { i } variant = 'body1' sx = { theme => ( { color : theme . palette . docker . blue [ 400 ] } ) } > { line . params . content } </ Typography >
336
+ }
337
+ return < pre key = { i } style = { { whiteSpace : 'pre-wrap' , display : 'inline' } } dangerouslySetInnerHTML = { { __html : convert . toHtml ( line . params . content ) } } />
338
+ }
339
+ if ( line . method === 'functions' ) {
340
+ return < Typography key = { i } variant = 'body1' sx = { { whiteSpace : 'pre-wrap' } } > { JSON . stringify ( line . params , null , 2 ) } </ Typography >
341
+ }
342
+ return < Typography key = { i } variant = 'body1' > { JSON . stringify ( line ) } </ Typography >
343
+ } ) }
344
+ </ div >
266
345
</ Paper >
267
346
)
268
347
}
0 commit comments