Skip to content

Commit fa0d673

Browse files
authored
Merge pull request #15 from docker/cm/readability
Improve readability
2 parents 7ac03a7 + 8c04171 commit fa0d673

File tree

6 files changed

+250
-186
lines changed

6 files changed

+250
-186
lines changed

src/extension/README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@ A Docker Desktop extension to run prompts.
44

55
https://github.com/docker/labs-ai-tools-for-devs
66

7-
![demo image](./image.png)
7+
## Usage
8+
9+
Install the extension from https://hub.docker.com/extensions/docker/labs-ai-tools-for-devs.
10+
11+
Enter your OpenAI key into the extension.
12+
13+
Add a project by selecting a directory.
14+
15+
Add a prompt by either pasting a git url/ref or selecting a local directory.
16+
17+
Make sure both your prompt and project are selected and then click "Run".
818

919
## Development
1020

@@ -13,13 +23,13 @@ You can use `docker` to build, install and push your extension. Also, we provide
1323
To build the extension, use `make build-extension` **or**:
1424

1525
```shell
16-
docker buildx build -t docker/labs-ai-tools-for-devs:latest . --load
26+
docker buildx build -t docker/labs-ai-tools-for-devs:local . --load
1727
```
1828

1929
To install the extension, use `make install-extension` **or**:
2030

2131
```shell
22-
docker extension install docker/labs-ai-tools-for-devs:latest
32+
docker extension install docker/labs-ai-tools-for-devs:local
2333
```
2434

2535
> If you want to automate this command, use the `-f` or `--force` flag to accept the warning message.
@@ -42,19 +52,19 @@ This starts a development server that listens on port `3000`.
4252
You can now tell Docker Desktop to use this as the frontend source. In another terminal run:
4353

4454
```shell
45-
docker extension dev ui-source vonwig/labs-ai-tools-for-devs:0.0.1 http://localhost:3000
55+
docker extension dev ui-source vonwig/labs-ai-tools-for-devs:local http://localhost:3000
4656
```
4757

4858
In order to open the Chrome Dev Tools for your extension when you click on the extension tab, run:
4959

5060
```shell
51-
docker extension dev debug docker/labs-ai-tools-for-devs:latest
61+
docker extension dev debug docker/labs-ai-tools-for-devs:local
5262
```
5363

5464
Each subsequent click on the extension tab will also open Chrome Dev Tools. To stop this behaviour, run:
5565

5666
```shell
57-
docker extension dev reset docker/labs-ai-tools-for-devs:latest
67+
docker extension dev reset docker/labs-ai-tools-for-devs:local
5868
```
5969

6070
### Backend development (optional)
@@ -67,7 +77,7 @@ Whenever you make changes in the [backend](./backend) source code, you will need
6777
Use the `docker extension update` command to remove and re-install the extension automatically:
6878

6979
```shell
70-
docker extension update docker/labs-ai-tools-for-devs:latest
80+
docker extension update docker/labs-ai-tools-for-devs:local
7181
```
7282

7383
> If you want to automate this command, use the `-f` or `--force` flag to accept the warning message.
@@ -79,5 +89,5 @@ docker extension update docker/labs-ai-tools-for-devs:latest
7989
To remove the extension:
8090

8191
```shell
82-
docker extension rm docker/labs-ai-tools-for-devs:latest
92+
docker extension rm docker/labs-ai-tools-for-devs:local
8393
```

src/extension/ui/src/App.tsx

Lines changed: 18 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
import React, { useEffect } from 'react';
2-
import Button from '@mui/material/Button';
3-
import DelIcon from '@mui/icons-material/Delete';
42
import { createDockerDesktopClient } from '@docker/extension-api-client';
5-
import { Chip, IconButton, Link, List, ListItem, ListItemButton, ListItemText, Paper, Stack, TextField, Typography } from '@mui/material';
3+
import { Paper, Stack, Typography, Button } from '@mui/material';
64
import { getRunArgs } from './args';
7-
import Convert from 'ansi-to-html';
5+
import OpenAIKey from './components/OpenAIKey';
6+
import Projects from './components/Projects';
7+
import Prompts from './components/Prompts';
8+
import RunOutput from './components/RunOutput';
89

9-
const convert = new Convert({ newline: true });
10-
11-
type RPCMessage = {
12-
jsonrpc?: string;
13-
method: string;
14-
params: any;
15-
}
16-
17-
// Note: This line relies on Docker Desktop's presence as a host application.
18-
// If you're running this React app in a browser, it won't work properly.
1910
const client = createDockerDesktopClient();
2011

2112
const track = (event: string) =>
@@ -42,7 +33,7 @@ export function App() {
4233

4334
const [promptInput, setPromptInput] = React.useState<string>('');
4435

45-
const [runOut, setRunOut] = React.useState<RPCMessage[]>([]);
36+
const [runOut, setRunOut] = React.useState<any[]>([]);
4637

4738
const scrollRef = React.useRef<HTMLDivElement>(null);
4839

@@ -53,13 +44,19 @@ export function App() {
5344
if (!selectedProject && projects.length > 0) {
5445
setSelectedProject(projects[0]);
5546
}
47+
if (selectedProject && projects.length === 0) {
48+
setSelectedPrompt(null);
49+
}
5650
}, [projects]);
5751

5852
useEffect(() => {
5953
localStorage.setItem('prompts', JSON.stringify(prompts));
6054
if (!selectedPrompt && prompts.length > 0) {
6155
setSelectedPrompt(prompts[0]);
6256
}
57+
if (selectedProject && prompts.length === 0) {
58+
setSelectedPrompt(null);
59+
}
6360
}, [prompts]);
6461

6562
useEffect(() => {
@@ -76,10 +73,7 @@ export function App() {
7673
}, [selectedPrompt]);
7774

7875
useEffect(() => {
79-
// URL format: https://github.com/<owner>/<repo>/tree/<branch>/<path>
80-
// REF format: github.com:<owner>/<repo>?ref=<branch>&path=<path>
8176
if (promptInput?.startsWith('http')) {
82-
// Convert URL to REF
8377
const url = new URL(promptInput);
8478
const registry = url.hostname.split('.').reverse().slice(1).reverse().join('.');
8579
const owner = url.pathname.split('/')[1];
@@ -97,12 +91,10 @@ export function App() {
9791
}
9892
}, [runOut]);
9993

100-
const delim = client.host.platform === 'win32' ? '\\' : '/';
101-
10294
const startPrompt = async () => {
10395
track('start-prompt');
104-
let output: RPCMessage[] = []
105-
const updateOutput = (line: RPCMessage) => {
96+
let output: any[] = []
97+
const updateOutput = (line: any) => {
10698
if (line.method === 'functions') {
10799
const functions = line.params;
108100
for (const func of functions) {
@@ -162,19 +154,6 @@ export function App() {
162154
}
163155
const json = JSON.parse(rpcMessage)
164156
updateOutput(json)
165-
// {
166-
// "jsonrpc": "2.0",
167-
// "method": "functions",
168-
// "params": [
169-
// {
170-
// "function": {
171-
// "name": "run-eslint",
172-
// "arguments": "{\n \""
173-
// },
174-
// "id": "call_53E2o4fq1QEmIHixWcKZmOqo"
175-
// }
176-
// ]
177-
// }
178157
}
179158
if (stderr) {
180159
updateOutput({ method: 'message', params: { debug: stderr } });
@@ -192,120 +171,9 @@ export function App() {
192171
return (
193172
<div style={{ overflow: 'auto', maxHeight: '100vh' }} ref={scrollRef}>
194173
<Stack direction="column" spacing={1}>
195-
<Paper sx={{ padding: 1 }}>
196-
<Typography variant='h3'>OpenAI Key</Typography>
197-
<TextField sx={{ mt: 1, width: '100%' }} onChange={e => setOpenAIKey(e.target.value)} value={openAIKey || ''} placeholder='Enter OpenAI API key' type='password' />
198-
</Paper>
199-
{/* Projects column */}
200-
<Paper sx={{ padding: 1 }}>
201-
<Typography variant='h3'>Projects</Typography>
202-
<Stack direction='row' spacing={1} sx={{ mt: 1 }} alignItems={'center'} justifyContent={'space-between'}>
203-
<Button sx={{ padding: 1 }} onClick={() => {
204-
client.desktopUI.dialog.showOpenDialog({
205-
properties: ['openDirectory', 'multiSelections']
206-
}).then((result) => {
207-
if (result.canceled) {
208-
return;
209-
}
210-
const newProjects = result.filePaths
211-
setProjects([...projects, ...newProjects]);
212-
});
213-
}}>
214-
Add project
215-
</Button>
216-
</Stack>
217-
<List>
218-
{projects.map((project) => (
219-
<ListItem
220-
key={project}
221-
sx={theme => ({ borderLeft: 'solid black 3px', borderColor: selectedProject === project ? theme.palette.success.main : 'none', my: 0.5, padding: 0 })}
222-
secondaryAction={
223-
<IconButton color='error' onClick={() => {
224-
// Confirm
225-
const confirm = window.confirm(`Are you sure you want to remove ${project}?`);
226-
if (!confirm) {
227-
return;
228-
}
229-
setProjects(projects.filter((p) => p !== project));
230-
}}>
231-
<DelIcon />
232-
</IconButton>
233-
}>
234-
<ListItemButton sx={{ padding: 0, pl: 1.5 }} onClick={() => {
235-
setSelectedProject(project);
236-
}}>
237-
<ListItemText primary={project.split(delim).pop()} secondary={project} />
238-
</ListItemButton>
239-
</ListItem>
240-
))}
241-
</List>
242-
</Paper>
243-
{/* Prompts column */}
244-
<Paper sx={{ padding: 1 }}>
245-
<Typography variant="h3">Prompts</Typography>
246-
<Stack direction='row' spacing={1} alignItems={'center'} justifyContent={'space-between'}>
247-
<TextField
248-
fullWidth
249-
placeholder='Enter GitHub ref or URL'
250-
value={promptInput}
251-
onChange={(e) => setPromptInput(e.target.value)}
252-
/>
253-
{promptInput.length > 0 && (
254-
<Button onClick={() => {
255-
setPrompts([...prompts, promptInput]);
256-
setPromptInput('');
257-
track('add-prompt');
258-
}}>Add prompt</Button>
259-
)}
260-
<Button onClick={() => {
261-
client.desktopUI.dialog.showOpenDialog({
262-
properties: ['openDirectory', 'multiSelections']
263-
}).then((result) => {
264-
if (result.canceled) {
265-
return;
266-
}
267-
track('add-local-prompt');
268-
setPrompts([...prompts, ...result.filePaths.map(p => `local://${p}`)]);
269-
});
270-
}}>Add local prompt</Button>
271-
</Stack>
272-
273-
<List>
274-
{prompts.map((prompt) => (
275-
<ListItem
276-
key={prompt}
277-
sx={theme => ({
278-
borderLeft: 'solid black 3px',
279-
borderColor: selectedPrompt === prompt ? theme.palette.success.main : 'none',
280-
my: 0.5,
281-
padding: 0
282-
})}
283-
secondaryAction={
284-
<IconButton color='error' onClick={() => {
285-
// Confirm
286-
const confirm = window.confirm(`Are you sure you want to remove ${prompt}?`);
287-
if (!confirm) {
288-
return;
289-
}
290-
setPrompts(prompts.filter((p) => p !== prompt));
291-
}}>
292-
<DelIcon />
293-
</IconButton>
294-
}>
295-
<ListItemButton sx={{ padding: 0, pl: 1.5 }} onClick={() => {
296-
setSelectedPrompt(prompt);
297-
}}>{
298-
prompt.startsWith('local://') ?
299-
<><ListItemText primary={<>{prompt.split(delim).pop()}<Chip sx={{ ml: 1 }} label='local' /></>} secondary={prompt.replace('local://', '')} /></>
300-
:
301-
<ListItemText primary={prompt.split('/').pop()} secondary={prompt} />
302-
}
303-
</ListItemButton>
304-
</ListItem>
305-
))}
306-
</List>
307-
</Paper>
308-
{/* Show row at bottom if selectProject AND selectedPrompt */}
174+
<OpenAIKey openAIKey={openAIKey || ''} setOpenAIKey={setOpenAIKey} />
175+
<Projects projects={projects} selectedProject={selectedProject} setProjects={setProjects} setSelectedProject={setSelectedProject} />
176+
<Prompts prompts={prompts} selectedPrompt={selectedPrompt} promptInput={promptInput} setPrompts={setPrompts} setSelectedPrompt={setSelectedPrompt} setPromptInput={setPromptInput} track={track} />
309177
{selectedProject && selectedPrompt && openAIKey ? (
310178
<Paper sx={{ padding: 1 }}>
311179
<Typography variant="h3">Ready</Typography>
@@ -323,35 +191,7 @@ export function App() {
323191
{openAIKey?.length ? null : <Typography variant='body1'> - OpenAI Key</Typography>}
324192
</Paper>
325193
)}
326-
{/* Show run output */}
327-
{
328-
runOut.length > 0 && (
329-
<Paper sx={{ p: 1 }}>
330-
<Stack direction='row' spacing={1} alignItems={'center'} justifyContent={'space-between'}>
331-
<Typography variant='h3'>Run output</Typography>
332-
<Button onClick={() => setShowDebug(!showDebug)}>{showDebug ? 'Hide' : 'Show'} debug</Button>
333-
</Stack>
334-
335-
<div style={{ overflow: 'auto', maxHeight: '100vh' }}>
336-
{runOut.map((line, i) => {
337-
if (line.method === 'message') {
338-
if (line.params.debug) {
339-
return showDebug ? <Typography key={i} variant='body1' sx={theme => ({ color: theme.palette.docker.grey[400] })}>{line.params.debug}</Typography> : null;
340-
}
341-
return <pre key={i} style={{ whiteSpace: 'pre-wrap', display: 'inline' }} dangerouslySetInnerHTML={{ __html: convert.toHtml(line.params.content) }} />
342-
}
343-
if (line.method === 'functions') {
344-
return <Typography key={i} variant='body1' sx={theme => ({ whiteSpace: 'pre-wrap', backgroundColor: theme.palette.docker.grey[300], p: 1 })}>{JSON.stringify(line.params, null, 2)}</Typography>
345-
}
346-
if (line.method === 'functions-done') {
347-
return showDebug ? <Typography key={i} variant='body1' sx={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(line.params, null, 2)}</Typography> : null;
348-
}
349-
return <Typography key={i} variant='body1'>{JSON.stringify(line)}</Typography>
350-
})}
351-
</div>
352-
</Paper>
353-
)
354-
}
194+
<RunOutput runOut={runOut} showDebug={showDebug} setShowDebug={setShowDebug} />
355195
</Stack>
356196
</div>
357197
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { Paper, TextField, Typography } from '@mui/material';
3+
4+
type OpenAIKeyProps = {
5+
openAIKey: string;
6+
setOpenAIKey: (key: string) => void;
7+
};
8+
9+
const OpenAIKey: React.FC<OpenAIKeyProps> = ({ openAIKey, setOpenAIKey }) => (
10+
<Paper sx={{ padding: 1 }}>
11+
<Typography variant='h3'>OpenAI Key</Typography>
12+
<TextField
13+
sx={{ mt: 1, width: '100%' }}
14+
onChange={e => setOpenAIKey(e.target.value)}
15+
value={openAIKey || ''}
16+
placeholder='Enter OpenAI API key'
17+
type='password'
18+
/>
19+
</Paper>
20+
);
21+
22+
export default OpenAIKey;

0 commit comments

Comments
 (0)