1
1
import React , { useEffect } from 'react' ;
2
2
import Button from '@mui/material/Button' ;
3
+ import DelIcon from '@mui/icons-material/Delete' ;
3
4
import { createDockerDesktopClient } from '@docker/extension-api-client' ;
4
- import { Divider , FormControlLabel , FormGroup , Grid , Link , List , ListItem , Modal , Paper , Stack , Switch , TextField , Tooltip , Typography } from '@mui/material' ;
5
- import { title } from 'process' ;
5
+ import { Divider , FormControlLabel , FormGroup , Grid , Icon , IconButton , Link , List , ListItem , ListItemButton , ListItemIcon , ListItemText , Modal , Paper , Stack , Switch , TextField , Tooltip , Typography } from '@mui/material' ;
6
+ import { getRunArgs } from './args' ;
7
+ import { on } from 'events' ;
6
8
7
9
// Note: This line relies on Docker Desktop's presence as a host application.
8
10
// If you're running this React app in a browser, it won't work properly.
9
11
const client = createDockerDesktopClient ( ) ;
10
12
13
+ const debounce = ( fn : Function , ms : number ) => {
14
+ let timeout : NodeJS . Timeout ;
15
+ return function ( ...args : any ) {
16
+ clearTimeout ( timeout ) ;
17
+ timeout = setTimeout ( ( ) => fn ( ...args ) , ms ) ;
18
+ } ;
19
+ }
20
+
21
+ const debouncedToastSuccess = debounce ( client . desktopUI . toast . success , 1000 )
22
+
11
23
export function App ( ) {
12
24
const [ projects , setProjects ] = React . useState < string [ ] > ( localStorage . getItem ( 'projects' ) ? JSON . parse ( localStorage . getItem ( 'projects' ) ! ) : [ ] ) ;
13
25
const [ selectedProject , setSelectedProject ] = React . useState < string | null > ( null ) ;
14
26
15
27
const [ prompts , setPrompts ] = React . useState < string [ ] > ( localStorage . getItem ( 'prompts' ) ? JSON . parse ( localStorage . getItem ( 'prompts' ) ! ) : [ ] ) ;
16
28
const [ selectedPrompt , setSelectedPrompt ] = React . useState < string | null > ( null ) ;
17
29
30
+ const [ openAIKey , setOpenAIKey ] = React . useState < string | null > ( null ) ;
31
+
32
+ const [ promptInput , setPromptInput ] = React . useState < string > ( '' ) ;
33
+
34
+ const [ runOut , setRunOut ] = React . useState < string > ( '' ) ;
35
+
18
36
useEffect ( ( ) => {
19
37
localStorage . setItem ( 'projects' , JSON . stringify ( projects ) ) ;
20
38
} , [ projects ] ) ;
@@ -23,58 +41,173 @@ export function App() {
23
41
localStorage . setItem ( 'prompts' , JSON . stringify ( prompts ) ) ;
24
42
} , [ prompts ] ) ;
25
43
44
+ useEffect ( ( ) => {
45
+ debouncedToastSuccess ( 'OpenAI key saved' ) ;
46
+ localStorage . setItem ( 'openAIKey' , openAIKey || '' ) ;
47
+ } , [ openAIKey ] ) ;
48
+
49
+
50
+ useEffect ( ( ) => {
51
+ // URL format: https://github.com/<owner>/<repo>/tree/<branch>/<path>
52
+ // REF format: github.com:<owner>/<repo>?ref=<branch>&path=<path>
53
+ if ( promptInput ?. startsWith ( 'http' ) ) {
54
+ // Convert URL to REF
55
+ const url = new URL ( promptInput ) ;
56
+ const registry = url . hostname
57
+ const owner = url . pathname . split ( '/' ) [ 1 ] ;
58
+ const repo = url . pathname . split ( '/' ) [ 2 ] ;
59
+ const branch = url . pathname . split ( '/' ) [ 4 ] ;
60
+ const path = url . pathname . split ( '/' ) . slice ( 5 ) . join ( '/' ) ;
61
+ const ref = `${ registry } :${ owner } /${ repo } ?ref=${ branch } &path=${ path } ` ;
62
+ setPromptInput ( ref ) ;
63
+ }
64
+ } , [ promptInput ] ) ;
65
+
66
+ const delim = client . host . platform === 'win32' ? '\\' : '/' ;
67
+
26
68
return (
27
69
< >
28
70
< Grid container spacing = { 2 } >
71
+ < Grid item xs = { 12 } >
72
+ < Paper sx = { { padding : 1 , pl : 2 } } >
73
+ < Stack direction = 'row' spacing = { 2 } alignItems = { 'center' } justifyContent = { 'space-between' } >
74
+ < Typography sx = { { flex : '1 1 30%' } } variant = 'h4' > OpenAI Key</ Typography >
75
+ < TextField sx = { { flex : '1 1 70%' } } onChange = { e => setOpenAIKey ( e . target . value ) } value = { openAIKey } placeholder = 'Enter OpenAI API key' type = 'password' />
76
+ </ Stack >
77
+ </ Paper >
78
+ </ Grid >
29
79
{ /* Projects column */ }
30
- < Grid item xs = { 4 } >
31
- < Paper >
32
- < Typography variant = "h6" component = "h2" > Projects</ Typography >
80
+ < Grid item xs = { 6 } >
81
+ < Paper sx = { { padding : 2 } } >
82
+ < Stack direction = 'row' spacing = { 2 } alignItems = { 'center' } justifyContent = { 'space-between' } >
83
+ < Typography variant = 'h2' sx = { { m : 2 , display : 'inline' } } > Projects</ Typography >
84
+ < Button sx = { { padding : 1 } } onClick = { ( ) => {
85
+ client . desktopUI . dialog . showOpenDialog ( {
86
+ properties : [ 'openDirectory' , 'multiSelections' ]
87
+ } ) . then ( ( result ) => {
88
+ if ( result . canceled ) {
89
+ return ;
90
+ }
91
+ const newProjects = result . filePaths
92
+ setProjects ( [ ...projects , ...newProjects ] ) ;
93
+ } ) ;
94
+ } } >
95
+ Add project
96
+ </ Button >
97
+ </ Stack >
33
98
< List >
34
99
{ projects . map ( ( project ) => (
35
- < ListItem key = { project } >
36
- < Button onClick = { ( ) => setSelectedProject ( project ) } > { project } </ Button >
100
+ < ListItem
101
+ sx = { theme => ( { borderLeft : 'solid black 3px' , borderColor : selectedProject === project ? theme . palette . success . main : 'none' , my : 0.5 , padding : 0 } ) }
102
+ secondaryAction = {
103
+ < IconButton color = 'error' onClick = { ( ) => {
104
+ // Confirm
105
+ const confirm = window . confirm ( `Are you sure you want to remove ${ project } ?` ) ;
106
+ if ( ! confirm ) {
107
+ return ;
108
+ }
109
+ setProjects ( projects . filter ( ( p ) => p !== project ) ) ;
110
+ } } >
111
+ < DelIcon />
112
+ </ IconButton >
113
+ } >
114
+ < ListItemButton sx = { { padding : 0 , pl : 1.5 } } onClick = { ( ) => {
115
+ setSelectedProject ( project ) ;
116
+ } } >
117
+ < ListItemText primary = { project . split ( delim ) . pop ( ) } secondary = { project } />
118
+ </ ListItemButton >
37
119
</ ListItem >
38
120
) ) }
39
121
</ List >
40
- < Button onClick = { ( ) => {
41
- const newProject = prompt ( 'Enter the name of the project' ) ;
42
- if ( newProject ) {
43
- setProjects ( [ ...projects , newProject ] ) ;
44
- }
45
- } } > Add project</ Button >
46
122
</ Paper >
47
123
</ Grid >
48
-
49
124
{ /* Prompts column */ }
50
- < Grid item xs = { 4 } >
51
- < Paper >
52
- < Typography variant = "h6" component = "h2" > Prompts</ Typography >
125
+ < Grid item xs = { 6 } >
126
+ < Paper sx = { { padding : 2 } } >
127
+ < Typography variant = "h2" component = "h2" > Prompts</ Typography >
128
+ < TextField
129
+ sx = { { width : '100%' } }
130
+ placeholder = 'Enter GitHub ref or URL'
131
+ value = { promptInput }
132
+ onChange = { ( e ) => setPromptInput ( e . target . value ) }
133
+ />
134
+ { promptInput . length > 0 && (
135
+ < Button onClick = { ( ) => {
136
+ setPrompts ( [ ...prompts , promptInput ] ) ;
137
+ setPromptInput ( '' ) ;
138
+ } } > Add prompt</ Button >
139
+ ) }
53
140
< List >
54
141
{ prompts . map ( ( prompt ) => (
55
- < ListItem key = { prompt } >
56
- < Button onClick = { ( ) => setSelectedPrompt ( prompt ) } > { prompt } </ Button >
142
+ < ListItem
143
+ sx = { theme => ( {
144
+ borderLeft : 'solid black 3px' ,
145
+ borderColor : selectedPrompt === prompt ? theme . palette . success . main : 'none' ,
146
+ my : 0.5 ,
147
+ padding : 0
148
+ } ) }
149
+ secondaryAction = {
150
+ < IconButton color = 'error' onClick = { ( ) => {
151
+ // Confirm
152
+ const confirm = window . confirm ( `Are you sure you want to remove ${ prompt } ?` ) ;
153
+ if ( ! confirm ) {
154
+ return ;
155
+ }
156
+ setPrompts ( prompts . filter ( ( p ) => p !== prompt ) ) ;
157
+ } } >
158
+ < DelIcon />
159
+ </ IconButton >
160
+ } >
161
+ < ListItemButton sx = { { padding : 0 , pl : 1.5 } } onClick = { ( ) => {
162
+ setSelectedPrompt ( prompt ) ;
163
+ } } >
164
+ < ListItemText primary = { prompt . split ( delim ) . pop ( ) } secondary = { prompt } />
165
+ </ ListItemButton >
57
166
</ ListItem >
58
167
) ) }
59
168
</ List >
60
- < Button onClick = { ( ) => {
61
- const newPrompt = prompt ( 'Enter the name of the prompt' ) ;
62
- if ( newPrompt ) {
63
- setPrompts ( [ ...prompts , newPrompt ] ) ;
64
- }
65
- } } > Add prompt</ Button >
66
169
</ Paper >
67
170
</ Grid >
68
171
{ /* Show row at bottom if selectProject AND selectedPrompt */ }
69
- { selectedProject && selectedPrompt && (
172
+ { selectedProject && selectedPrompt ? (
173
+ < Grid item xs = { 12 } >
174
+ < Paper sx = { { padding : 1 } } >
175
+ < Typography variant = "h3" component = "h2" > Ready</ Typography >
176
+ < Typography > < pre > PROJECT={ selectedProject } </ pre > </ Typography >
177
+ < Typography > < pre > PROMPT={ selectedPrompt } </ pre > </ Typography >
178
+ < Button sx = { { mt : 1 , } } color = 'success' onClick = { async ( ) => {
179
+ // Write openai key to $HOME/.openai-api-key
180
+ setRunOut ( 'Writing OpenAI key...' ) ;
181
+ await client . extension . vm ?. cli . exec ( '/bin/sh' , [ '-c' , `echo ${ openAIKey } > $HOME/.openai-api-key` ] ) ;
182
+ setRunOut ( 'Running...' ) ;
183
+ client . docker . cli . exec ( 'run' , getRunArgs ( selectedPrompt , selectedProject , client . host . hostname , client . host . platform ) , {
184
+ stream : {
185
+ onError : ( err ) => {
186
+ setRunOut ( runOut + '\n' + err . message ) ;
187
+ } ,
188
+ onOutput : ( { stdout, stderr } ) => {
189
+ setRunOut ( runOut + '\n' + ( stdout || stderr ) ) ;
190
+ }
191
+ }
192
+ } )
193
+ } } >
194
+ < Typography variant = 'h3' > Run</ Typography >
195
+ </ Button >
196
+ </ Paper >
197
+ </ Grid >
198
+ ) : (
70
199
< Grid item xs = { 12 } >
71
200
< Paper >
72
- < Typography variant = "h6" component = "h2" > Selected</ Typography >
73
- < Typography > Project: { selectedProject } </ Typography >
74
- < Typography > Prompt: { selectedPrompt } </ Typography >
75
- < Button onClick = { ( ) => {
76
- client . docker . cli . exec ( 'docker' , [ 'run' ] )
77
- } } > Run { selectedPrompt } in { selectedProject } </ Button >
201
+ You must select a project and a prompt to run.
202
+ </ Paper >
203
+ </ Grid >
204
+ ) }
205
+ { /* Show run output */ }
206
+ { runOut && (
207
+ < Grid item xs = { 12 } >
208
+ < Paper >
209
+ < Typography variant = 'h3' > Run output</ Typography >
210
+ < pre > { runOut } </ pre >
78
211
</ Paper >
79
212
</ Grid >
80
213
) }
0 commit comments