It works! Web app calls wasp-cli and works.

This commit is contained in:
Martin Sosic 2023-06-16 15:43:16 +02:00
parent 3ee1a419b9
commit b1778cd505
6 changed files with 152 additions and 30 deletions

View File

@ -2,10 +2,19 @@ app waspAi {
wasp: {
version: "^0.10.6"
},
title: "wasp-ai"
title: "wasp-ai",
dependencies: [("uuid", "^9.0.0")]
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@client/MainPage.jsx"
}
action startGeneratingNewApp {
fn: import { startGeneratingNewApp } from "@server/operations.js"
}
query getAppGenerationResult {
fn: import { getAppGenerationResult } from "@server/operations.js"
}

View File

@ -1,35 +1,46 @@
import waspLogo from './waspLogo.png'
import './Main.css'
import { useState } from 'react'
import startGeneratingNewApp from '@wasp/actions/startGeneratingNewApp'
import getAppGenerationResult from '@wasp/queries/getAppGenerationResult'
import { useQuery } from '@wasp/queries'
const MainPage = () => {
const [appName, setAppName] = useState('')
const [appDesc, setAppDesc] = useState('')
const [inputDisabled, setInputDisabled] = useState(false)
const [appId, setAppId] = useState('')
const [generationDone, setGenerationDone] = useState(false)
const { data: appGenerationResult } = useQuery(
getAppGenerationResult,
{ appId },
{ enabled: !!appId && !generationDone, refetchInterval: 3000 }
)
const fillInExampleAppDetails = () => {
if (appGenerationResult?.status === 'success' || appGenerationResult?.status === 'failure') {
if (!generationDone) {
setGenerationDone(true)
}
}
const logs = appGenerationResult?.messages.filter(m => m.type === 'log').map(m => m.text)
let files = {}
{
appGenerationResult?.messages.filter(m => m.type === 'write-file').map(m => m.text.split('\n')).forEach(([path, ...contentLines]) => {
files[path] = contentLines.join('\n')
})
}
function fillInExampleAppDetails () {
setAppName('TodoApp')
setAppDesc('A simple todo app with one main page that lists all the tasks. I can create new tasks, or toggle existing ones.'
+ 'User owns tasks. User can only see and edit their own tasks. Tasks are saved in the database.')
}
const generate = () => {
async function startGenerating () {
if (!(appName && appDesc)) {
return window.alert('Please enter an app name and description.')
}
setInputDisabled(true)
window.alert('Generating app. This may take a while. Please wait.')
// Idea #1:
// We could use websockets -> we first send the message to start generating the app, then we pick up messages from the server.
// We would need to identify this specific client probably, so that the server knows whom to send messages to.
// We could do that by sending some unique UUID we generate on client for new request, and server would send that UUID back.
// But wait, then all the clients can see those messages coming?
// I think we need to be able to open a websocket room that only our client and server see.
// Idea #2:
// We use action to start the generation, and it runs as long as generation runs.
// It stores the results of generation into the local variable, under the UUID specific for this generation.
// We can then use another action, that we call regularly (every second) from client, to get the results of generation, and we can use the UUID to get the results.
// It would need to be smart about how to correctly consume piece of the results, but I think we can handle that.
setAppId(await startGeneratingNewApp({ appName, appDesc }))
}
return (
@ -48,7 +59,7 @@ const MainPage = () => {
placeholder="AppName"
value={appName}
onChange={(e) => setAppName(e.target.value)}
disabled={inputDisabled}
disabled={appId}
/>
</div>
@ -59,12 +70,33 @@ const MainPage = () => {
rows="5"
cols="50"
onChange={(e) => setAppDesc(e.target.value)}
disabled={inputDisabled}
disabled={appId}
/>
</div>
<button disabled={inputDisabled} onClick={() => fillInExampleAppDetails()}>Fill in with example app details</button>
<button onClick={() => generate()}>Generate</button>
<button disabled={appId} onClick={() => fillInExampleAppDetails()}>Fill in with example app details</button>
<button onClick={() => startGenerating()}>Generate</button>
{ appId && !generationDone && (
<div> Generating... </div>
)}
<div key="logs">
{ logs && logs.map((log, i) =>
<div style={{"backgroundColor": "yellow"}} key={i}>
{log}
</div>
) }
</div>
<div key="files">
{ files && Object.keys(files).map((path) =>
<div key={path}>
<div> {path}: </div>
<div style={{"backgroundColor": "grey"}}> {files[path]} </div>
</div>
) }
</div>
</main>
</div>
)

View File

@ -0,0 +1,75 @@
import { v4 as uuidv4 } from 'uuid'
import { spawn } from 'child_process'
// TODO: This will keep growing and growing, in memory forever. We need to clean up old results!
// Or maybe let's even save it in the database?
const appGenerationResults = {}
export async function startGeneratingNewApp (args, _context) {
const appId = uuidv4()
appGenerationResults[appId] = {
status: 'in-progress',
messages: [],
unconsumedStdout: ''
}
let waspCliProcess = null
if (process.env.NODE_ENV === 'production') {
waspCliProcess = spawn('wasp', ['new-ai', args.appName, args.appDesc])
} else {
// NOTE: In dev when we use `wasp-cli`, we want to make sure that if this app is run via `wasp` that its datadir env var does not propagate,
// so we reset it here. This is problem only if you run app with `wasp` and let it call `wasp-cli` here.
waspCliProcess = spawn('wasp-cli', ['new-ai', args.appName, args.appDesc], { env: { ...process.env, waspc_datadir: undefined}})
}
waspCliProcess.stdout.on('data', (data) => {
console.log(data.toString())
const newStdoutChunk = data.toString()
let unconsumedStdout = appGenerationResults[appId].unconsumedStdout + newStdoutChunk
let newMessages = []
while (true) {
const firstMsgEndHeaderMatch = unconsumedStdout.match(/===\/ WASP AI: (LOG|WRITE FILE) ====/)
if (firstMsgEndHeaderMatch) {
const msgEndHeaderStartIdx = firstMsgEndHeaderMatch.index
const msgEndHeader = firstMsgEndHeaderMatch[0]
const msgStartHeader = msgEndHeader.replace('===/', '====') + '\n'
const msgStartHeaderMatch = unconsumedStdout.match(new RegExp(msgStartHeader))
const msgStartHeaderStartIdx = msgStartHeaderMatch.index
const message = {
text: unconsumedStdout.substring(msgStartHeaderStartIdx + msgStartHeader.length, msgEndHeaderStartIdx),
type: msgStartHeader === '==== WASP AI: LOG ====\n' ? 'log' : 'write-file'
}
newMessages = [ ...newMessages, message]
unconsumedStdout = unconsumedStdout.substring(msgEndHeaderStartIdx + msgEndHeader.length)
} else {
break
}
}
appGenerationResults[appId].messages = [ ...appGenerationResults[appId].messages, ...newMessages]
appGenerationResults[appId].unconsumedStdout = unconsumedStdout
})
waspCliProcess.stderr.on('data', (data) => {
console.error(data.toString())
})
waspCliProcess.on('close', (code) => {
console.log('WASP CLI PROCESS STOPPED')
if (code === 0) {
appGenerationResults[appId].status = 'success'
} else {
appGenerationResults[appId].status = 'failure'
}
});
return appId
}
export async function getAppGenerationResult (args, _context) {
const appId = args.appId
return {
status: appGenerationResults[appId].status,
messages: appGenerationResults[appId].messages
}
}

View File

@ -20,6 +20,7 @@ import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectS
generateNewProject :: NewProjectDetails -> CodeAgent ()
generateNewProject newProjectDetails = do
writeToLog "Generating project skeleton..."
(waspFilePath, planRules) <- generateAndWriteProjectSkeleton newProjectDetails
writeToLog "Generated project skeleton."

View File

@ -17,6 +17,7 @@ import StrongPath.Operations ()
import System.Directory (createDirectoryIfMissing, setCurrentDirectory)
import System.Environment (lookupEnv)
import System.FilePath (takeDirectory)
import System.IO (hFlush, stdout)
import Wasp.Cli.Command (Command, CommandError (CommandError))
import qualified Wasp.Cli.Command.AI.CodeAgent as CA
import qualified Wasp.Cli.Command.AI.GenerateNewProject as GNP
@ -63,22 +64,24 @@ newForMachine webAppName webAppDescription = do
let codeAgentConfig =
CA.CodeAgentConfig
{ CA._openAIApiKey = openAIApiKey,
CA._writeFile = \fp c ->
CA._writeFile = \fp c -> do
let fpT = T.pack fp
in T.IO.putStrLn . ("\n" <>) $
[trimming|
==== WASP AI: WRITE FILE ====
${fpT}
${c}
===/ WASP AI: WRITE FILE ====
|],
CA._writeLog = \msg ->
T.IO.putStrLn . ("\n" <>) $
[trimming|
==== WASP AI: WRITE FILE ====
${fpT}
${c}
===/ WASP AI: WRITE FILE ====
|]
hFlush stdout,
CA._writeLog = \msg -> do
T.IO.putStrLn . ("\n" <>) $
[trimming|
==== WASP AI: LOG ====
${msg}
===/ WASP AI: LOG ====
|]
hFlush stdout
}
liftIO $

View File

@ -49,6 +49,8 @@ data-files:
Cli/templates/**/*.jsx
Cli/templates/**/*.png
Cli/templates/**/*.ts
-- TODO: Do these files with dots in front cause us problems?
-- I know from before they were problematic, we should test it thoroughly!
Cli/templates/new/.gitignore
Cli/templates/new/.wasproot
Cli/templates/new/src/.waspignore