diff --git a/backend/src/Wasm/Libs/Editor.fs b/backend/src/Wasm/Libs/Editor.fs index 9bfde28d9d..07adb87349 100644 --- a/backend/src/Wasm/Libs/Editor.fs +++ b/backend/src/Wasm/Libs/Editor.fs @@ -124,7 +124,7 @@ let fns : List = let source = Json.Vanilla.deserialize sourceJson let stdLib = - LibExecution.StdLib.combine [ StdLibExecution.StdLib.contents ] [] [] + LibExecution.StdLib.combine [ StdLibExecution.StdLib.contents; Wasm.Libs.HttpClient.contents ] [] [] let! result = let expr = exprsCollapsedIntoOne source.exprs diff --git a/backend/src/Wasm/Wasm.fsproj b/backend/src/Wasm/Wasm.fsproj index 2eb4febd5c..cd80dfd74d 100644 --- a/backend/src/Wasm/Wasm.fsproj +++ b/backend/src/Wasm/Wasm.fsproj @@ -30,8 +30,8 @@ - + diff --git a/canvases/dark-client/client.dark b/canvases/dark-client/client.dark new file mode 100644 index 0000000000..1d6d11dd11 --- /dev/null +++ b/canvases/dark-client/client.dark @@ -0,0 +1,479 @@ +// TODO: let ignore (z: ): Unit +module Prelude = + let alert (s: String): Unit = + let _ = WASM.Editor.callJSFunction "alert" [s] + () + + let log (s: String): Unit = + let _ = WASM.Editor.callJSFunction "console.log" [s] + () + + let rUnwrap (result: Result<'a, 'b>): 'a = + match result with + | Ok s -> s + | Error e -> + log e // TODO: this won't work if non-string + alert "Expected OK, got Error - see log" + + let parseAndSerializeProgram (userProgram: String): String = + let response = + WASM.HttpClient.request + "POST" + "http://dark-editor.dlio.localhost:11003/get-program-json" + [] + (String.toBytes userProgram) + + response |> rUnwrap |> (fun r -> r.body) |> String.fromBytes + + +module OpenAI = + // todo: throw this stuff into an OpenAI module? + type OpenAIChatCompletionRequestMessage = + { role: String; content: String } + type OpenAIChatCompletionRequest = + { model: String + max_tokens: Int + temperature: Float + messages : List } + + type OpenAIChatCompletionResponseChoiceMessage = { content: String } + type OpenAIChatCompletionResponseChoice = { message: OpenAIChatCompletionResponseChoiceMessage } + type OpenAIChatCompletionResponse = { choices: List } + + let openAIcompletion (prompt: String): Result = + let apiKey = + let url = "http://dark-editor.dlio.localhost:11003/openai-apikey-yikes" + match WASM.HttpClient.request "get" url [] Bytes.empty with + | Ok response -> response.body |> String.fromBytes + + let openAIRequest = + OpenAIChatCompletionRequest + { model = "gpt-3.5-turbo" + max_tokens = 700 + temperature = 0.7 + messages = [OpenAIChatCompletionRequestMessage {role = "user"; content = prompt}] } + + match Json.serialize openAIRequest with + | Ok reqBody -> + let headers = + [ + ("authorization", "Bearer " ++ apiKey) + ("content-type", "application/json") + ] + + let openAIResponse = + WASM.HttpClient.request "POST" "https://api.openai.com/v1/chat/completions" headers (String.toBytes reqBody) + + match openAIResponse with + | Ok r -> + match Json.parse (String.fromBytes r.body) with + | Ok r -> + match List.head r.choices with + | Just c -> Ok c.message.content + + | Nothing -> Error ("No choices returned") + | Error err -> Error ("Couldn't parse open ai completino response - " ++ err) + | Error e -> Error ("OpenAI API request failed\n" ++ e) + | Error e -> Error ("Couldn't serialize request" ++ e) + + +module Html = + // module to be used in the dark editor to write HTML + type Attribute = { key: String; value: String } + + let attr (key: String) (value: String): Attribute = + { key = key; value = value } + + let br () = tag "br" [] [] + let str (s: String): String = s + let comment (s: String): String = "" + + + + let document (nodes: List): String = + let htmlDoc = "" + let theRest = nodes + |>String::join "" + htmlDoc ++ theRest + + let tag (name: String) (attributes: List) (children: List): String = + let attributesText = + attributes + |> List.map (fun attr -> attr.key ++ "=" ++ attr.value ++ "") + |> String.join " " + let startTag = "<" ++ tag ++ " " ++ attributesText ++ ">" + let endTag = "" + let childHtml = String.join children "" + startTag ++ childHtml ++ endTag + + + let a (attributes: List) (children: List): String = + tag "a" attributes children + + // same for div, span, etc.: + let div (attributes: List) (children: List): String = + tag "div" attributes children + + let span (attributes: List) (children: List): String = + tag "span" attributes children + + let h1 (attributes: List) (children: List): String = + tag "h1" attributes children + + let h2 (attributes: List) (children: List): String = + tag "h2" attributes children + + let h3 (attributes: List) (children: List): String = + tag "h3" attributes children + + let h4 (attributes: List) (children: List): String = + tag "h4" attributes children + + let h5 (attributes: List) (children: List): String = + tag "h5" attributes children + + let h6 (attributes: List) (children: List): String = + tag "h6" attributes children + + let p (attributes: List) (children: List): String = + tag "p" attributes children + + let ul (attributes: List) (children: List): String = + tag "ul" attributes children + + let ol (attributes: List) (children: List): String = + tag "ol" attributes children + + let li (attributes: List) (children: List): String = + tag "li" attributes children + + let table (attributes: List) (children: List): String = + tag "table" attributes children + + let tr (attributes: List) (children: List): String = + tag "tr" attributes children + + let td (attributes: List) (children: List): String = + tag "td" attributes children + + let th (attributes: List) (children: List): String = + tag "th" attributes children + + let tbody (attributes: List) (children: List): String = + tag "tbody" attributes children + + let thead (attributes: List) (children: List): String = + tag "thead" attributes children + + let tfoot (attributes: List) (children: List): String = + tag "tfoot" attributes children + + let caption (attributes: List) (children: List): String = + tag "caption" attributes children + + let colgroup (attributes: List) (children: List): String = + tag "colgroup" attributes children + + let col (attributes: List) (children: List): String = + tag "col" attributes children + + +// 100k token (200pg book) context windows +module Poe = + let foo = "bar" + + + +// TODO: in the prompt, include something like +// "all code snippets returned must be executable without any other context needed" + +// Model + +let view<'a> (a: 'a): String = + """< + """ + +type View<'a> = { view: 'a -> String } // html + +type Code = | Code of code: String + +type UserProgram = + { Id: String + Name: String + CodeSnippets: List // these are concated after being sorted by type + Docs: List + Secrets: List } + +type CodeSnippetType = Type | Function | Script | View | HttpEndpoint +type CodeSnippet = { id: String; name: String; type: CodeSnippetType code: Code } +type Secret = { id: String; key: String; value: String } +type Doc = { id: String; name: String; text: String } + + + + +// call upon "CanvasHack" with a UserProgram +// and refresh their application + + +type EditorFocus = + | Home + | EditCode of relevantCodePieces: List + + +type GptResponsePart = + | UpdatedSnippet of code: String // todo: tidy to smaller 'diff' later + | Text + | Options + +type UIActionAvailable = + | + +type AvailableAction = + | EvalCode + | AddFunction + | AddType + | UpdateType + | Other of prompt: String // resolve later + +type GptResponse = + { UpdatedSnippet: Option + Text: Option + AvailableActions: List } + + +type EditorContext = + { program: UserProgram + focus: EditorFocus } + + +type BotResponseItem = + | Text of text: String + | CodeSnippet of id: String + +type ChatHistoryItem = + | UserPrompt of id: String * prompt: String + | BotResponse of id: String * items: List // todo: add promptId to tie together with relevant UserPrompt + +type CodeSnippet = + { id: String; code: String; eval: Option } + +type Model = + { systemPrompt: String + chatHistory: List + codeSnippets: List } + + +type Msg = + | UserGavePrompt of prompt: String + | UserRequestedCodeEval of id: String * codeSnippet: String + +let update (model: Model) (msg: Msg) : Model = + match msg with + | UserGavePrompt userPrompt -> + // I guess, until we have cmds or something, + // we have to deal with http calls and such in-line, like here + let fullPrompt = model.systemPrompt ++ userPrompt + + match openAIcompletion fullPrompt with + | Ok apiResponse -> + let (newCodeSnippets, botResponseParts) = + (String.split_v1 (" " ++ apiResponse) "```") + |> List.indexedMap (fun i text -> (i, text)) + |> List.fold ([], []) (fun acc curr -> + let (codeSnippets, responseParts) = acc + let (i, text) = curr + let text = String.trim text + + if text == "" then + acc + else + if i % 2 == 1 then + let snippetId = rUnwrap(String.random_v2 5) + let updatedCodeSnippets = List.append codeSnippets [CodeSnippet { id = snippetId; code = text; eval = Nothing }] + let updatedResponseParts = List.append responseParts [BotResponseItem.CodeSnippet snippetId] + (updatedCodeSnippets, updatedResponseParts) + else + codeSnippets, (List.append responseParts [BotResponseItem.Text text]) + ) + + let newChatItemsItems = [ + ChatHistoryItem.UserPrompt (rUnwrap(String.random_v2 5)) userPrompt + ChatHistoryItem.BotResponse (rUnwrap(String.random_v2 5)) botResponseParts + ] + + Model + { systemPrompt = model.systemPrompt + chatHistory = List.append model.chatHistory newChatItemsItems + codeSnippets = List.append model.codeSnippets newCodeSnippets } + + | Error err -> alert err + + | UserRequestedCodeEval id codeSnippet -> + // split this into 2 groups - the one we're updating, and the rest + let (snippetToUpdate, otherSnippets) = + model.codeSnippets |> List.partition (fun cs -> cs.id == id) + + match List.head snippetToUpdate with + | Nothing -> alert "couldn't find snippet" + | Just snippetToUpdate -> + let parsedAndSerialized = parseAndSerializeProgram codeSnippet + + let evalResult = (WASM.Editor.evalUserProgram parsedAndSerialized) |> rUnwrap + + let updatedCodeSnippets = + List.append + [CodeSnippet { id = snippetToUpdate.id; code = codeSnippet; eval = Just evalResult }] + otherSnippets + + Model + { systemPrompt = model.systemPrompt + chatHistory = model.chatHistory + codeSnippets = updatedCodeSnippets } + + + /// Single point of communicating to JS host + /// + /// Let the JS host know that the state has been updated, + /// so that it can update the UI accordingly. + let updateStateInJS (newState: Model): Result = + match Json.serialize newState with + | Ok serialized -> + let _ = WASM.Editor.callJSFunction "window.stateUpdated" [serialized] + Ok () + | Error err -> + let _ = WASM.Editor.callJSFunction "console.warn" ["Couldn't serialize - " ++ err] + Error "Couldn't serialize updated state" + + + /// Single point of communication from JS host + /// + /// Listen for events from the JS host, and update the state accordingly. + let handleEvent (evt: String): Result = + match Json.parse evt with + | Ok msg -> + match WASM.Editor.getState with + | Ok currentState -> + let newState = update currentState msg + + // returns result, but let's assume it worked... + let _ = WASM.Editor.setState newState + + updateStateInJS newState + + | Error err -> Error "Couldn't get current state" + | Error err -> Error "Couldn't parse raw msg" + + +// Init +// (things to run on startup, before accepting any events +// the initial state is set to the result of the final expr) +let wordsAboutDark = + // we need a reasonably-sophisticated way of doing this + WASM.HttpClient.request "get" "http://dark-editor.dlio.localhost:11003/system-prompt" [] Bytes.empty + |> rUnwrap + +let systemPrompt = "" + +let initState = + match systemPrompt with + | Ok response -> + let demoSnippet = + CodeSnippet + { id = (String.random_v2 5) |> rUnwrap + code = "let incr (i: Int): Int = i + 1\n\nincr 5" + eval = Nothing } + + let chatHistory = [ + ChatHistoryItem.UserPrompt + (rUnwrap(String.random_v2 5)) + "Hello, I want a function that increments an integer!" + + ChatHistoryItem.BotResponse + (rUnwrap(String.random_v2 5)) + [ + BotResponseItem.Text "OK - here's some code:" + BotResponseItem.CodeSnippet demoSnippet.id + ] + ] + + Model + { systemPrompt = String.fromBytes response.body + chatHistory = chatHistory + codeSnippets = [demoSnippet] } + + | Error err -> + Model + { systemPrompt = String.fromBytes "nope" + chatHistory = [ ] } + +updateStateInJS initState + +initState + + + + + + + + + +// // Models to interop with JS host +// // (TODO: wrap this in `module JS = `) +// type BotResponseJS + +// type ChatHistoryItemJS = +// | BotResponse of { author : string; body : String } + +// type ModelForJS = +// // should be 1:1 with Model, but nice and serializeable +// { systemPrompt : string +// state : string +// code: String +// chatHistory: List } + +// // TODO: result +// let stateForJS (state: Model) : ModelForJS = +// { systemPrompt = state.systemPrompt +// state = match state.State with +// | WaitingForFirstInput -> "WaitingForFirstInput" +// | WaitingForUserInput -> "WaitingForUserInput" +// | BotIsThinking -> "BotIsThinking" +// chatHistory = +// state.chatHistory +// |> List.map (fun item -> +// { author = match item.author with User -> "User" | Bot -> "Bot" +// prompt = item.text }) } + +// // TODO: result +// let modelFromJS (model: ModelForJS) : Model = +// { SystemPrompt = Prompt model.systemPrompt +// State = +// match model.state with +// | "WaitingForFirstInput" -> WaitingForFirstInput +// | "WaitingForUserInput" -> WaitingForUserInput +// | "BotIsThinking" -> BotIsThinking +// ChatHistory = +// model.chatHistory +// |> List.map (fun item -> +// { Author = match item.author with "User" -> User | "Bot" -> Bot +// Prompt = item.text }) } + + +// type EventJS = { eventName: String; data: String } + +// let msgOfJS (msg: EventJS) : Msg = +// match msg.eventName with +// | "userGavePrompt" -> UserGavePrompt (Prompt msg.data) +// | "botResponded" -> BotResponded (Prompt msg.data) +// | _ -> failwith "Couldn't parse event name" + +// let msgToJS (msg: Msg) : EventJS = +// match msg with +// | UserGavePrompt prompt -> +// { eventName = "userGavePrompt" +// data = match prompt with Prompt p -> p } +// | BotResponded prompt -> +// { eventName = "botResponded" +// data = match prompt with Prompt p -> p } \ No newline at end of file diff --git a/canvases/dark-client/config.yml b/canvases/dark-client/config.yml new file mode 100644 index 0000000000..d00b3b6c83 --- /dev/null +++ b/canvases/dark-client/config.yml @@ -0,0 +1,12 @@ +version: 2 +id: 11111111-1111-1111-1111-111111111111 +main: main +client: + - client/prelude.dark + - client/html.dark + - client/openai.dark + - client/model.dark + - client/update.dark + - client/view.dark + - client/main.dark + diff --git a/canvases/dark-client/main.dark b/canvases/dark-client/main.dark new file mode 100644 index 0000000000..e1fe879216 --- /dev/null +++ b/canvases/dark-client/main.dark @@ -0,0 +1,72 @@ +[] +let _handler _req = + Http.response (String.toBytes OPENAI_API_KEY) 200 + +[] +let _handler _req = + let filePath = "dark-editor-vue/dist/index.html" + let file = Experiments.readFromCanvases filePath + let body = + match file with + | Ok f -> f + | Error _ -> Bytes.empty + + Http.responseWithHeaders + (body) + (Dict.fromListOverwritingDuplicates [ ("Content-Type", "text/html") ]) + 200 + +[] +let _handler _req = + match Experiments.readFromCanvases ("dark-editor-vue/dist/assets/" ++ path) with + | Ok fileContents -> + let contentType = + match (String.split_v1 path ".") |> List.last_v2 |> Option.withDefault "" with + | "js" -> "application/javascript" + | "json" -> "application/json" + | "css" -> "text/css" + | _ -> "text/plain" + + let headers = + Dict.fromListOverwritingDuplicates [ ("content-type", contentType) ] + + Http.responseWithHeaders fileContents headers 200 + + | Error _e -> Http.response (String.toBytes "Couldn't load asset") 400 + + +[] +let _handler _req = + match Experiments.readFromCanvases "dark-editor/client.dark" with + | Ok sourceInBytes -> Http.response sourceInBytes 200 + | Error _err -> Http.response (String.toBytes "Couldn't load client.dark from disk") 500 + + +// TODO: remove once a parser can run in WASM runtime +[] +let _handler _req = + let sourceInBytes = request.body + let program = + Experiments.parseAndSerializeProgram (String.fromBytes sourceInBytes) "user-code.dark" + + match program with + | Ok program -> + let types = Option.withDefault (Dict.get_v2 program "types") "[]" + let fns = Option.withDefault (Dict.get_v2 program "fns") "[]" + let exprs = Option.withDefault (Dict.get_v2 program "exprs") "[]" + + let json = "{ \"types\": " ++ types ++ ", \"fns\": " ++ fns ++ ", \"exprs\": " ++ exprs ++ "}" + + Http.responseWithHeaders + (String.toBytes json) + (Dict.fromListOverwritingDuplicates [ ("content-type", "application-json")]) + 200 + + | Error err -> Http.response (String.toBytes ("Couldn't parse the program \n" ++ err)) 400 + + +[] +let _handler _req = + match Experiments.readFromCanvases "dark-editor/system-prompt.txt" with + | Ok prompt -> Http.response prompt 200 + | Error _e -> Http.response (String.toBytes "Couldn't load prompt from disk") 500 diff --git a/canvases/dark-client/system-prompt.txt b/canvases/dark-client/system-prompt.txt new file mode 100644 index 0000000000..fcc3fbbf44 --- /dev/null +++ b/canvases/dark-client/system-prompt.txt @@ -0,0 +1,161 @@ +You are a tool that is used by Dark developers to write and manipulate their Dark +programs. + +Dark is a programming language used to write backends. In addition to the language, +it has built-in support for DBs, REPLs, CRONs, Workers, etc. The term "toplevels" is +used to refer to these distinct parts of a user's program - a user DB, a REPL, a +CRON, etc. + +Dark is very similar to F#, including syntax and semantics, with these exceptions: + +- Dark does not support "record deconstruction" in `match` expressions +- field and function names use camelCase in Dark, not snake_case +- Dark does not support mutation. +- all names must be fully-qualified (eg `String.trimLeft`) and imports (or `open`) are not supported +- string interpolation replaces `sprintf`, for example `$"My name is {name}"` +--- + +# Dark features: + +## Types: + +Dark is statically typed, and supports type definitions. Types are good and should be used often. +- records are supported, such as `type MyType = { x : int }` +- enums are supported, such as `type MyType = | OptionOne of Int | SecondOption | Another of Int * String * Bool` + +## Functions: + +Dark supports functions at the top level, but not within other functions. Functions are good and should be used often. +- functions are defined using `let` +- all parameter types must be specified (i.e. `String` or `Int`) and are capitalized +- the return type must also be specified + +for example: + +let increment (x: Int) : Int = + x + 1 + +## HTTP handlers + +HTTP handlers are expressions, with each handler stored in a .dark file. + +A request has a `request` variable available: +`{ url: String; headers: List; body: Bytes }` + +The expression should return a response with this type: +`{ statusCode: int; headers: List; body: byte[] }` + +With each response display a list a variables that needs to be filled by the user + +For example: + +hello.dark +``` +[] +let _handler _req = + let body = ("hello " ++ user )|> String.toBytes + Http.response body 200 +``` +variables: +user + + +## Standard library + +type HttpResponse = { + statusCode: int + headers: List + body: Bytes +} + +Here are some functions available in Dark: +- DB.set (dbName: String) (item: 'a) : Uuid +- DB.get (dbName: String) (key: String) : 'a +- HttpClient.request (method: String) (url: String) (headers: List) (body: Bytes) : HttpResponse +- String.fromBytes (bytes: Bytes): String +- String.toBytes (text: String): Bytes +- Json.toString<'a> (v: 'a): String +- Json.parse<'a> (j: String) : 'a + +These are the available standard library modules: +- Int +- Character +- String +- Float +- Bool +- List +- Option +- Result +- DB +- HttpClient +- Json + +## Json + +To use Json, create types that match the JSON request and response, and use the types +to create and parse json. For example: + +``` +type TwilioRequest = { + message : String + from: String + to: String + retries: Int +} + +type TwilioResponse = { + error : Option + successCode : Int +} + +let request = ... +let requestBody = Json.serialize(request) +... +let response = Json.parse(responseBody) +``` + +Only use type names, not anonymous type definitions. + +## Secrets + +Secrets are supported. Secrets are string variables named using ALL_CAPS, such as +`TWILIO_APIKEY` or `AWS_OIDC_FEDERATION_ID`. They are already available, you do not +need to call any functions. + +--- + +Your job is to assist in building an application in Dark. +When you reply, ensure all code samples are executable without any other context. +Below all types and function defined, include at least one expression to be evaluated. +Within the code snippets, do not add code comments. +All code snippets should be wrapped in ``` tags. + +Here's an example of a prompt/response combination: + +User: build a small program that models a list of People, +a function that returns the age of a Person, then hardcode a list of people +and return the age of each person + +You: +Ok, no problem. +``` +type Person = { + name: String + age: Int +} + +let getAge (p: Person) : Int = + p.age + +let people = [ + { name = "Alice"; age = 20 } + { name = "Bob"; age = 30 } +] + +people |> List.map getAge +``` + +Hope that helps! + +--- + diff --git a/canvases/dark-client/template.secrets b/canvases/dark-client/template.secrets new file mode 100644 index 0000000000..a7c42eae8d --- /dev/null +++ b/canvases/dark-client/template.secrets @@ -0,0 +1,3 @@ +#copy this to just .secrets and fill in the values + +OPENAI_API_KEY=TEST \ No newline at end of file diff --git a/canvases/dark-client/wordmark-dark-transparent.png b/canvases/dark-client/wordmark-dark-transparent.png new file mode 100644 index 0000000000..ff5232a4ac Binary files /dev/null and b/canvases/dark-client/wordmark-dark-transparent.png differ diff --git a/canvases/dark-editor-stachu/client.dark b/canvases/dark-editor-stachu/client.dark new file mode 100644 index 0000000000..dd1d9144e6 --- /dev/null +++ b/canvases/dark-editor-stachu/client.dark @@ -0,0 +1,428 @@ +// TODO: let ignore (z: ): Unit +module Prelude = + let alert (s: String): Unit = + let _ = WASM.Editor.callJSFunction "alert" [s] + () + + let log (s: String): Unit = + let _ = WASM.Editor.callJSFunction "console.log" [s] + () + + let rUnwrap (result: Result<'a, 'b>): 'a = + match result with + | Ok s -> s + | Error e -> + log e // TODO: this won't work if non-string + alert "Expected OK, got Error - see log" + + let parseAndSerializeProgram (userProgram: String): String = + let response = + WASM.HttpClient.request + "POST" + "http://dark-editor.dlio.localhost:11003/get-program-json" + [] + (String.toBytes userProgram) + + response |> rUnwrap |> (fun r -> r.body) |> String.fromBytes + + +module OpenAI = + type ChatCompletionRequestMessage = + { role: String; content: String } + type ChatCompletionRequest = + { model: String + max_tokens: Int + temperature: Float + messages : List } + + type ChatCompletionResponseChoiceMessage = { content: String } + type ChatCompletionResponseChoice = { message: ChatCompletionResponseChoiceMessage } + type ChatCompletionResponse = { choices: List } + + let completion (prompt: String): Result = + let apiKey = + let url = "http://dark-editor.dlio.localhost:11003/openai-apikey-yikes" + match WASM.HttpClient.request "get" url [] Bytes.empty with + | Ok response -> response.body |> String.fromBytes + + let request = + ChatCompletionRequest + { model = "gpt-3.5-turbo" + max_tokens = 700 + temperature = 0.7 + messages = [ChatCompletionRequestMessage {role = "user"; content = prompt}] } + + match Json.serialize Request with + | Ok reqBody -> + let headers = + [ + ("authorization", "Bearer " ++ apiKey) + ("content-type", "application/json") + ] + + let response = + WASM.HttpClient.request "POST" "https://api.openai.com/v1/chat/completions" headers (String.toBytes reqBody) + + match response with + | Ok r -> + match Json.parse (String.fromBytes r.body) with + | Ok r -> + match List.head r.choices with + | Just c -> Ok c.message.content + + | Nothing -> Error ("No choices returned") + | Error err -> Error ("Couldn't parse open ai completino response - " ++ err) + | Error e -> Error ("OpenAI API request failed\n" ++ e) + | Error e -> Error ("Couldn't serialize request" ++ e) + + +module Html = + // module to be used in the dark editor to write HTML + type Attribute = { key: String; value: String } + + let attr (key: String) (value: String): Attribute = + { key = key; value = value } + + let br () = tag "br" [] [] + let str (s: String): String = s + let comment (s: String): String = "" + + + + let document (nodes: List): String = + let htmlDoc = "" + let theRest = nodes + |>String::join "" + htmlDoc ++ theRest + + let tag (name: String) (attributes: List) (children: List): String = + let attributesText = + attributes + |> List.map (fun attr -> attr.key ++ "=" ++ attr.value ++ "") + |> String.join " " + let startTag = "<" ++ tag ++ " " ++ attributesText ++ ">" + let endTag = "" + let childHtml = String.join children "" + startTag ++ childHtml ++ endTag + + + let a (attributes: List) (children: List): String = + tag "a" attributes children + + // same for div, span, etc.: + let div (attributes: List) (children: List): String = + tag "div" attributes children + + let span (attributes: List) (children: List): String = + tag "span" attributes children + + let h1 (attributes: List) (children: List): String = + tag "h1" attributes children + + let h2 (attributes: List) (children: List): String = + tag "h2" attributes children + + let h3 (attributes: List) (children: List): String = + tag "h3" attributes children>) (children: List): String = + tag "h6" attributes children + + let p (attributes: List) (children: List): String = + tag "p" attributes children + + let ul (attributes: List) (children: List): String = + tag "ul" attributes children + + let ol (attributes: List) (children: List): String = + tag "ol" attributes children + + let li (attributes: List) (children: List): String = + tag "li" attributes children + + + +// 100k token (~200pg book) context windows +module Poe = + let foo = "bar" + + + +// TODO: in the prompt, include something like +// "all code snippets returned must be executable without any other context needed" + +// Model + +let view<'a> (a: 'a): String = + """< + """ + +type View<'a> = { view: 'a -> String } // html + +type Code = | Code of code: String + +type UserProgram = + { Id: String + Name: String + Functions: List + Types: List + HttpEndpoints: List + Data: List + Context: List + Scripts: List