11import React , { useEffect } from 'react' ;
22import Button from '@mui/material/Button' ;
3+ import DelIcon from '@mui/icons-material/Delete' ;
34import { 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' ;
68
79// Note: This line relies on Docker Desktop's presence as a host application.
810// If you're running this React app in a browser, it won't work properly.
911const client = createDockerDesktopClient ( ) ;
1012
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+
1123export function App ( ) {
1224 const [ projects , setProjects ] = React . useState < string [ ] > ( localStorage . getItem ( 'projects' ) ? JSON . parse ( localStorage . getItem ( 'projects' ) ! ) : [ ] ) ;
1325 const [ selectedProject , setSelectedProject ] = React . useState < string | null > ( null ) ;
1426
1527 const [ prompts , setPrompts ] = React . useState < string [ ] > ( localStorage . getItem ( 'prompts' ) ? JSON . parse ( localStorage . getItem ( 'prompts' ) ! ) : [ ] ) ;
1628 const [ selectedPrompt , setSelectedPrompt ] = React . useState < string | null > ( null ) ;
1729
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+
1836 useEffect ( ( ) => {
1937 localStorage . setItem ( 'projects' , JSON . stringify ( projects ) ) ;
2038 } , [ projects ] ) ;
@@ -23,58 +41,173 @@ export function App() {
2341 localStorage . setItem ( 'prompts' , JSON . stringify ( prompts ) ) ;
2442 } , [ prompts ] ) ;
2543
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+
2668 return (
2769 < >
2870 < 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 >
2979 { /* 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 >
3398 < List >
3499 { 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 >
37119 </ ListItem >
38120 ) ) }
39121 </ 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 >
46122 </ Paper >
47123 </ Grid >
48-
49124 { /* 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+ ) }
53140 < List >
54141 { 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 >
57166 </ ListItem >
58167 ) ) }
59168 </ 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 >
66169 </ Paper >
67170 </ Grid >
68171 { /* 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+ ) : (
70199 < Grid item xs = { 12 } >
71200 < 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 >
78211 </ Paper >
79212 </ Grid >
80213 ) }
0 commit comments