Actions are now generated.

This commit is contained in:
Martin Sosic 2023-06-15 15:44:52 +02:00
parent cbb3962508
commit 9ffc115fcd
9 changed files with 279 additions and 89 deletions

View File

@ -56,6 +56,7 @@ getFile path = gets $ H.lookup path . _files
getAllFiles :: CodeAgent [(FilePath, Text)] getAllFiles :: CodeAgent [(FilePath, Text)]
getAllFiles = gets $ H.toList . _files getAllFiles = gets $ H.toList . _files
-- TODO: Make it so that if ChatGPT replies with being too busy, we try again.
queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text queryChatGPT :: ChatGPTParams -> [ChatMessage] -> CodeAgent Text
queryChatGPT params messages = do queryChatGPT params messages = do
key <- asks _openAIApiKey key <- asks _openAIApiKey

View File

@ -10,7 +10,9 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import NeatInterpolation (trimming) import NeatInterpolation (trimming)
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToLog) import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToLog)
import Wasp.Cli.Command.AI.GenerateNewProject.Action (Action, generateAndWriteAction)
import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (..)) import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (..))
import Wasp.Cli.Command.AI.GenerateNewProject.Entity (writeEntitiesToWaspFile)
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (generatePlan) import Wasp.Cli.Command.AI.GenerateNewProject.Plan (generatePlan)
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeleton) import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectSkeleton)
@ -25,18 +27,18 @@ import Wasp.Cli.Command.AI.GenerateNewProject.Skeleton (generateAndWriteProjectS
-- and also contain description of what happened (or maybe that is separate message). -- and also contain description of what happened (or maybe that is separate message).
generateNewProject :: NewProjectDetails -> CodeAgent () generateNewProject :: NewProjectDetails -> CodeAgent ()
generateNewProject newProjectDetails = do generateNewProject newProjectDetails = do
waspFilePath <- generateAndWriteProjectSkeleton newProjectDetails (waspFilePath, planRules) <- generateAndWriteProjectSkeleton newProjectDetails
writeToLog "Generated project skeleton." writeToLog "Generated project skeleton."
writeToLog "Generating plan..." writeToLog "Generating plan..."
plan <- generatePlan newProjectDetails plan <- generatePlan newProjectDetails planRules
writeToLog $ "Plan generated!\n" <> summarizePlan plan writeToLog $ "Plan generated!\n" <> summarizePlan plan
writeEntitiesToWaspFile waspFilePath (Plan.entities plan) writeEntitiesToWaspFile waspFilePath (Plan.entities plan)
writeToLog "Added entities to wasp file." writeToLog "Added entities to wasp file."
writeToLog "Generating actions..." writeToLog "Generating actions..."
actions <- forM (Plan.actions plan) $ generateAndWriteAction waspFilePath plan actions <- forM (Plan.actions plan) $ generateAndWriteAction newProjectDetails waspFilePath plan
writeToLog "Generating queries..." writeToLog "Generating queries..."
queries <- forM (Plan.queries plan) $ generateAndWriteQuery waspFilePath plan queries <- forM (Plan.queries plan) $ generateAndWriteQuery waspFilePath plan
@ -70,16 +72,9 @@ generateNewProject newProjectDetails = do
showT :: Show a => a -> Text showT :: Show a => a -> Text
showT = T.pack . show showT = T.pack . show
generateAndWriteAction waspFilePath plan actionPlan = do
action <- generateAction newProjectDetails (Plan.entities plan) actionPlan
writeActionToFile action
writeActionToWaspFile waspFilePath action
writeToLog $ "Generated action: " <> T.pack (Plan.actionName actionPlan)
return action
generateAndWriteQuery waspFilePath plan queryPlan = do generateAndWriteQuery waspFilePath plan queryPlan = do
query <- generateQuery newProjectDetails (Plan.entities plan) queryPlan query <- generateQuery newProjectDetails (Plan.entities plan) queryPlan
writeQueryToFile query writeQueryToFile query -- TODO: Do we need to convert @server/ from path into src/server/?
writeQueryToWaspFile waspFilePath query writeQueryToWaspFile waspFilePath query
writeToLog $ "Generated query: " <> T.pack (Plan.queryName queryPlan) writeToLog $ "Generated query: " <> T.pack (Plan.queryName queryPlan)
return query return query
@ -91,26 +86,6 @@ generateNewProject newProjectDetails = do
writeToLog $ "Generated page: " <> T.pack (Plan.pageName pagePlan) writeToLog $ "Generated page: " <> T.pack (Plan.pageName pagePlan)
return page return page
writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent ()
writeEntitiesToWaspFile waspFilePath entities = do
-- TODO: assemble code for each entity and write it to wasp file.
undefined
generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action
generateAction = undefined
writeActionToFile :: Action -> CodeAgent ()
writeActionToFile = undefined
writeActionToWaspFile :: FilePath -> Action -> CodeAgent ()
writeActionToWaspFile waspFilePath action = undefined
data Action = Action
{ _actionWaspDecl :: String,
_actionJsImpl :: String,
_actionPlan :: Plan.Action
}
generateQuery :: NewProjectDetails -> [Plan.Entity] -> Plan.Query -> CodeAgent Query generateQuery :: NewProjectDetails -> [Plan.Entity] -> Plan.Query -> CodeAgent Query
generateQuery = undefined generateQuery = undefined

View File

@ -0,0 +1,149 @@
{-# LANGUAGE DeriveGeneric #-}
module Wasp.Cli.Command.AI.GenerateNewProject.Action
( generateAndWriteAction,
Action,
)
where
import Data.Aeson (FromJSON)
import Data.List (isPrefixOf)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import GHC.Generics (Generic)
import NeatInterpolation (trimming)
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeToFile, writeToLog)
import Wasp.Cli.Command.AI.GenerateNewProject.Common
( NewProjectDetails (..),
defaultChatGPTParams,
queryChatGPTForJSON,
)
import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts
import Wasp.Cli.Command.AI.GenerateNewProject.Entity (entityPlanToWaspDecl)
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (Plan)
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
generateAndWriteAction :: NewProjectDetails -> FilePath -> Plan -> Plan.Action -> CodeAgent Action
generateAndWriteAction newProjectDetails waspFilePath plan actPlan = do
action <- generateAction newProjectDetails (Plan.entities plan) actPlan
writeActionToJsFile action
writeActionToWaspFile waspFilePath action
writeToLog $ "Generated action: " <> T.pack (Plan.actionName actPlan)
return action
generateAction :: NewProjectDetails -> [Plan.Entity] -> Plan.Action -> CodeAgent Action
generateAction newProjectDetails entityPlans plan = do
impl <- queryChatGPTForJSON defaultChatGPTParams chatMessages
return Action {actionImpl = impl, actionPlan = plan}
where
chatMessages =
[ ChatMessage {role = System, content = Prompts.systemPrompt},
ChatMessage {role = User, content = planPrompt}
]
appName = T.pack $ _projectAppName newProjectDetails
appDesc = T.pack $ _projectDescription newProjectDetails
basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo
actionName = T.pack $ Plan.actionName plan
actionFnPath = T.pack $ Plan.actionFnPath plan
actionDesc = T.pack $ Plan.actionDesc plan
entityDecls = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans
planPrompt =
[trimming|
${basicWaspLangInfoPrompt}
${actionDocPrompt}
We are implementing a Wasp app (check bottom for description).
This app has following entities:
${entityDecls}
Let's now implement the following Wasp action:
- name: ${actionName}
- fn: ${actionFnPath}
- description: ${actionDesc}
Please, respond ONLY with a valid JSON, of following format:
{ "actionWaspDecl": "action ${actionName} {\n ... }",
"actionJsImpl": "export const {$actionName} ... ",
"actionJsImports": "import foo from \"bar.js\"\n ...""
}
"waspDeclaration" and "jsImplementation" are required, "jsImports" you can skip if none are needed.
There should be no other text in the response.
${appDescriptionStartMarkerLine}
App name: ${appName}
${appDesc}
|]
actionDocPrompt =
[trimming|
Action is implemented via Wasp declaration and corresponding NodeJS implementation.
Example of Wasp declaration:
```wasp
action updateTaskIsDone {
fn: import { updateTaskIsDone } from "@server/taskActions.js",
entities: [Task] // Entities that action uses.
}
```
Example of NodeJS implementation:
```js
import HttpError from '@wasp/core/HttpError.js'
export const updateTaskIsDone = (args, context) => {
if (!context.user) { throw new HttpError(403) }
return context.entities.Task.update({ // prisma object
where: { args.id },
data: { args.isDone }
})
}
```
Action can then be easily called from the client, via Wasp's RPC mechanism.
|]
writeActionToJsFile :: Action -> CodeAgent ()
writeActionToJsFile action =
-- TODO: An issue we have here is that if other action already did the same import,
-- we don't know and we import it again.
-- One thing we can do it supply chatGPT with a list of imports that are already there.
-- Second thing we can do is to look for same lines at the start of the file, but that sounds fragile.
-- Maybe best to read and pass previous imports (we would have to do that above somewhere).
-- Or even the whole file? Hmmmmm.
writeToFile path $
((jsImports <> "\n") <>) . (<> "\n\n" <> jsImpl) . (fromMaybe "")
where
path = resolvePath $ Plan.actionFnPath $ actionPlan action
jsImpl = T.pack $ actionJsImpl $ actionImpl action
jsImports = T.pack $ actionJsImports $ actionImpl action
resolvePath p | "@server/" `isPrefixOf` p = "src/server/" <> drop (length ("@server/" :: String)) p
resolvePath _ = error "path incorrectly formatted, should start with @server."
writeActionToWaspFile :: FilePath -> Action -> CodeAgent ()
writeActionToWaspFile waspFilePath action =
writeToFile waspFilePath $
(<> "\n\n" <> waspDeclCode) . fromMaybe (error "wasp file shouldn't be empty")
where
waspDeclCode = T.pack $ actionWaspDecl $ actionImpl action
data Action = Action
{ actionImpl :: ActionImpl,
actionPlan :: Plan.Action
}
deriving (Show)
data ActionImpl = ActionImpl
{ actionWaspDecl :: String,
actionJsImpl :: String,
actionJsImports :: String
}
deriving (Generic, Show)
instance FromJSON ActionImpl

View File

@ -2,10 +2,19 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Common
( NewProjectDetails (..), ( NewProjectDetails (..),
File, File,
AuthProvider (..), AuthProvider (..),
queryChatGPTForJSON,
defaultChatGPTParams,
writeToWaspFileEnd,
) )
where where
import Data.Aeson (FromJSON)
import qualified Data.Aeson as Aeson
import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT, writeToFile)
import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), Model (GPT_3_5_turbo_16k))
import Wasp.Util (naiveTrimJSON, textToLazyBS)
data NewProjectDetails = NewProjectDetails data NewProjectDetails = NewProjectDetails
{ _projectAppName :: !String, { _projectAppName :: !String,
@ -13,8 +22,29 @@ data NewProjectDetails = NewProjectDetails
_projectAuth :: !AuthProvider _projectAuth :: !AuthProvider
} }
-- TODO: Make these relative to WaspProjectDir? -- TODO: Make these relative to WaspProjectDir, via StrongPath?
type File = (FilePath, Text) type File = (FilePath, Text)
-- TODO: Support more methods. -- TODO: Support more methods.
data AuthProvider = UsernameAndPassword data AuthProvider = UsernameAndPassword
queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a
queryChatGPTForJSON chatGPTParams chatMessages = do
responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages
case Aeson.eitherDecode $ textToLazyBS responseJSONText of
Right plan -> return plan
Left _errMsg ->
-- TODO: Handle this better.
-- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it.
-- Write to log, to let user know.
error "Failed to parse plan"
-- TODO: Test more for the optimal temperature (possibly higher).
-- TODO: Should we make sure we have max_tokens set to high enough?
defaultChatGPTParams :: ChatGPTParams
defaultChatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0}
writeToWaspFileEnd :: FilePath -> Text -> CodeAgent ()
writeToWaspFileEnd waspFilePath text = do
writeToFile waspFilePath $
(<> "\n" <> text) . fromMaybe (error "wasp file shouldn't be empty")

View File

@ -0,0 +1,28 @@
module Wasp.Cli.Command.AI.GenerateNewProject.Entity
( writeEntitiesToWaspFile,
entityPlanToWaspDecl,
)
where
import Data.Text (Text)
import qualified Data.Text as T
import NeatInterpolation (trimming)
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent)
import Wasp.Cli.Command.AI.GenerateNewProject.Common (writeToWaspFileEnd)
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Plan as Plan
writeEntitiesToWaspFile :: FilePath -> [Plan.Entity] -> CodeAgent ()
writeEntitiesToWaspFile waspFilePath entityPlans = do
writeToWaspFileEnd waspFilePath $ "\n" <> entitiesCode
where
entitiesCode = T.intercalate "\n\n" $ entityPlanToWaspDecl <$> entityPlans
entityPlanToWaspDecl :: Plan.Entity -> Text
entityPlanToWaspDecl plan =
let name = T.pack $ Plan.entityName plan
pslBody = T.pack $ Plan.entityBodyPsl plan
in [trimming|
entity ${name} {=psl
${pslBody}
psl=}
|]

View File

@ -7,38 +7,32 @@ module Wasp.Cli.Command.AI.GenerateNewProject.Plan
Action (..), Action (..),
Page (..), Page (..),
generatePlan, generatePlan,
PlanRule,
) )
where where
import Data.Aeson (FromJSON) import Data.Aeson (FromJSON)
import qualified Data.Aeson as Aeson
import Data.ByteString.Lazy.UTF8 (ByteString)
import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import GHC.Generics (Generic) import GHC.Generics (Generic)
import NeatInterpolation (trimming) import NeatInterpolation (trimming)
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, queryChatGPT) import Wasp.Cli.Command.AI.CodeAgent (CodeAgent)
import Wasp.Cli.Command.AI.GenerateNewProject.Common (NewProjectDetails (_projectAppName, _projectDescription)) import Wasp.Cli.Command.AI.GenerateNewProject.Common
( AuthProvider (UsernameAndPassword),
NewProjectDetails (..),
defaultChatGPTParams,
queryChatGPTForJSON,
)
import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine) import Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts (appDescriptionStartMarkerLine)
import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts import qualified Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts as Prompts
import Wasp.OpenAI.ChatGPT (ChatGPTParams (..), ChatMessage (..), ChatRole (..), Model (..)) import Wasp.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..))
generatePlan :: NewProjectDetails -> CodeAgent Plan -- | Additional rule to follow while generating plan.
generatePlan newProjectDetails = do type PlanRule = String
responseJSONText <- naiveTrimJSON <$> queryChatGPT chatGPTParams chatMessages
case Aeson.eitherDecode $ textToLazyBS responseJSONText of generatePlan :: NewProjectDetails -> [PlanRule] -> CodeAgent Plan
Right plan -> return plan generatePlan newProjectDetails planRules = do
Left _errMsg -> queryChatGPTForJSON defaultChatGPTParams chatMessages
-- TODO: Handle this better.
-- Try sending response back to chatGPT and ask it to fix it -> hey it is not valid JSON, fix it.
-- Write to log, to let user know.
error "Failed to parse plan"
where where
-- TODO: Try configuring temperature.
-- TODO: Make sure we have max_tokens set to high enough.
chatGPTParams = ChatGPTParams {_model = GPT_3_5_turbo_16k, _temperature = Just 1.0}
chatMessages = chatMessages =
[ ChatMessage {role = System, content = Prompts.systemPrompt}, [ ChatMessage {role = System, content = Prompts.systemPrompt},
ChatMessage {role = User, content = planPrompt} ChatMessage {role = User, content = planPrompt}
@ -47,6 +41,7 @@ generatePlan newProjectDetails = do
appDesc = T.pack $ _projectDescription newProjectDetails appDesc = T.pack $ _projectDescription newProjectDetails
basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo basicWaspLangInfoPrompt = Prompts.basicWaspLangInfo
waspFileExamplePrompt = Prompts.waspFileExample waspFileExamplePrompt = Prompts.waspFileExample
rulesText = T.pack . unlines $ "Rules:" : map (" - " ++) planRules
planPrompt = planPrompt =
[trimming| [trimming|
${basicWaspLangInfoPrompt} ${basicWaspLangInfoPrompt}
@ -80,9 +75,10 @@ generatePlan newProjectDetails = do
}] }]
} }
Make sure to generate at least one page with routePath "/". ${rulesText}
We will later use this plan to implement all of these parts of Wasp app. We will later use this plan to implement all of these parts of Wasp app,
so make sure descriptions are detailed enough to guide implementing them.
Please, respond ONLY with a valid JSON that is a plan. Please, respond ONLY with a valid JSON that is a plan.
There should be no other text in the response. There should be no other text in the response.
@ -171,15 +167,3 @@ data Page = Page
deriving (Generic, Show) deriving (Generic, Show)
instance FromJSON Page instance FromJSON Page
-- | Given a text containing a single instance of JSON and some text around it but no { or }, trim
-- it until just JSON is left.
-- Examples
-- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }"
-- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly.
naiveTrimJSON :: Text -> Text
naiveTrimJSON textContainingJson =
T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson
textToLazyBS :: Text -> ByteString
textToLazyBS = TLE.encodeUtf8 . TL.fromStrict

View File

@ -10,15 +10,16 @@ import NeatInterpolation (trimming)
import qualified StrongPath as SP import qualified StrongPath as SP
import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeNewFile) import Wasp.Cli.Command.AI.CodeAgent (CodeAgent, writeNewFile)
import Wasp.Cli.Command.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..)) import Wasp.Cli.Command.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..))
import Wasp.Cli.Command.AI.GenerateNewProject.Plan (PlanRule)
import Wasp.Cli.Command.CreateNewProject (readCoreWaspProjectFiles) import Wasp.Cli.Command.CreateNewProject (readCoreWaspProjectFiles)
import qualified Wasp.Version import qualified Wasp.Version
generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent FilePath generateAndWriteProjectSkeleton :: NewProjectDetails -> CodeAgent (FilePath, [PlanRule])
generateAndWriteProjectSkeleton newProjectDetails = do generateAndWriteProjectSkeleton newProjectDetails = do
coreFiles <- liftIO $ map (first SP.fromRelFile) <$> readCoreWaspProjectFiles coreFiles <- liftIO $ map (first SP.fromRelFile) <$> readCoreWaspProjectFiles
mapM_ writeNewFile coreFiles mapM_ writeNewFile coreFiles
let waspFile@(waspFilePath, _) = generateBaseWaspFile newProjectDetails let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
writeNewFile waspFile writeNewFile waspFile
case _projectAuth newProjectDetails of case _projectAuth newProjectDetails of
@ -28,28 +29,36 @@ generateAndWriteProjectSkeleton newProjectDetails = do
writeNewFile generateDotEnvServerFile writeNewFile generateDotEnvServerFile
return waspFilePath return (waspFilePath, planRules)
generateBaseWaspFile :: NewProjectDetails -> File generateBaseWaspFile :: NewProjectDetails -> (File, [PlanRule])
generateBaseWaspFile newProjectDetails = (path, content) generateBaseWaspFile newProjectDetails = ((path, content), planRules)
where where
path = "main.wasp" path = "main.wasp"
appName = T.pack $ _projectAppName newProjectDetails appName = T.pack $ _projectAppName newProjectDetails
appTitle = appName appTitle = appName
waspVersion = T.pack $ show Wasp.Version.waspVersion waspVersion = T.pack $ show Wasp.Version.waspVersion
appAuth = case _projectAuth newProjectDetails of (appAuth, authPlanRules) = case _projectAuth newProjectDetails of
-- NOTE: We assume here that there will be a page with route "/". -- NOTE: We assume two things here:
-- - that there will be a page with route "/".
-- - that there will be a User entity with 'id', 'username' and 'password'.
-- We later that those will be added to Plan.
UsernameAndPassword -> UsernameAndPassword ->
[trimming| ( [trimming|
auth: { auth: {
userEntity: User, userEntity: User,
methods: { methods: {
usernameAndPassword: {} usernameAndPassword: {}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
}, },
onAuthFailedRedirectTo: "/login", |],
onAuthSucceededRedirectTo: "/" [ "Generate a User entity, with at least the following fields: 'id', 'username', 'password'.",
}, "There should be a page with route path \"/\"."
|] ]
)
planRules = authPlanRules <> ["Don't generate the Login or Signup page."]
content = content =
[trimming| [trimming|
app ${appName} { app ${appName} {
@ -60,12 +69,6 @@ generateBaseWaspFile newProjectDetails = (path, content)
${appAuth} ${appAuth}
} }
entity User {=psl
id Int @id @default(autoincrement())
username String @unique
password String
psl=}
route LoginRoute { path: "/login", to: LoginPage } route LoginRoute { path: "/login", to: LoginPage }
page LoginPage { page LoginPage {
component: import Login from "@client/Login.jsx" component: import Login from "@client/Login.jsx"

View File

@ -33,6 +33,8 @@ module Wasp.Util
kebabToCamelCase, kebabToCamelCase,
maybeToEither, maybeToEither,
whenM, whenM,
naiveTrimJSON,
textToLazyBS,
) )
where where
@ -41,6 +43,7 @@ import Control.Monad (unless, when)
import qualified Crypto.Hash.SHA256 as SHA256 import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.UTF8 as BSU import qualified Data.ByteString.UTF8 as BSU
import Data.Char (isSpace, isUpper, toLower, toUpper) import Data.Char (isSpace, isUpper, toLower, toUpper)
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
@ -48,8 +51,11 @@ import Data.List (intercalate)
import Data.List.Split (splitOn, wordsBy) import Data.List.Split (splitOn, wordsBy)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as TextEncoding import qualified Data.Text.Encoding as TextEncoding
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import StrongPath (File, Path') import StrongPath (File, Path')
import qualified StrongPath as SP import qualified StrongPath as SP
import Text.Printf (printf) import Text.Printf (printf)
@ -230,3 +236,15 @@ maybeToEither leftValue = maybe (Left leftValue) Right
getEnvVarDefinition :: (String, String) -> String getEnvVarDefinition :: (String, String) -> String
getEnvVarDefinition (name, value) = concat [name, "=", value] getEnvVarDefinition (name, value) = concat [name, "=", value]
-- | Given a text containing a single instance of JSON and some text around it but no { or }, trim
-- it until just JSON is left.
-- Examples
-- naiveTrimJson "some text { \"a\": 5 } yay" == "{\"a\": 5 }"
-- naiveTrimJson "some {text} { \"a\": 5 }" -> won't work correctly.
naiveTrimJSON :: Text -> Text
naiveTrimJSON textContainingJson =
T.reverse . T.dropWhile (/= '}') . T.reverse . T.dropWhile (/= '{') $ textContainingJson
textToLazyBS :: Text -> BSL.ByteString
textToLazyBS = TLE.encodeUtf8 . TL.fromStrict

View File

@ -372,8 +372,10 @@ library cli-lib
Wasp.Cli.FileSystem Wasp.Cli.FileSystem
Wasp.Cli.Command.AI.CodeAgent Wasp.Cli.Command.AI.CodeAgent
Wasp.Cli.Command.AI.GenerateNewProject Wasp.Cli.Command.AI.GenerateNewProject
Wasp.Cli.Command.AI.GenerateNewProject.Action
Wasp.Cli.Command.AI.GenerateNewProject.Common Wasp.Cli.Command.AI.GenerateNewProject.Common
Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts Wasp.Cli.Command.AI.GenerateNewProject.Common.Prompts
Wasp.Cli.Command.AI.GenerateNewProject.Entity
Wasp.Cli.Command.AI.GenerateNewProject.Plan Wasp.Cli.Command.AI.GenerateNewProject.Plan
Wasp.Cli.Command.AI.GenerateNewProject.Skeleton Wasp.Cli.Command.AI.GenerateNewProject.Skeleton
Wasp.Cli.Command.AI.New Wasp.Cli.Command.AI.New