mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-25 10:03:07 +03:00
It works! Web app calls wasp-cli and works.
This commit is contained in:
parent
3ee1a419b9
commit
b1778cd505
@ -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"
|
||||
}
|
@ -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>
|
||||
)
|
||||
|
75
wasp-ai/src/server/operations.js
Normal file
75
wasp-ai/src/server/operations.js
Normal 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
|
||||
}
|
||||
}
|
@ -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."
|
||||
|
||||
|
@ -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 $
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user