diff --git a/pgml-dashboard/Cargo.lock b/pgml-dashboard/Cargo.lock index e14050a5b..0acfe1334 100644 --- a/pgml-dashboard/Cargo.lock +++ b/pgml-dashboard/Cargo.lock @@ -2535,7 +2535,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pgml" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -2613,6 +2613,7 @@ dependencies = [ "sentry-log", "serde", "serde_json", + "sqlparser", "sqlx", "tantivy", "time", @@ -3928,6 +3929,15 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "sqlparser" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0272b7bb0a225320170c99901b4b5fb3a4384e255a7f2cc228f61e2ba3893e75" +dependencies = [ + "log", +] + [[package]] name = "sqlx" version = "0.7.3" diff --git a/pgml-dashboard/Cargo.toml b/pgml-dashboard/Cargo.toml index 71dbbcf4b..1c1b7aa8a 100644 --- a/pgml-dashboard/Cargo.toml +++ b/pgml-dashboard/Cargo.toml @@ -43,6 +43,7 @@ sentry = "0.31" sentry-log = "0.31" sentry-anyhow = "0.31" serde_json = "1" +sqlparser = "0.38" sqlx = { version = "0.7.3", features = [ "runtime-tokio-rustls", "postgres", "json", "migrate", "time", "uuid", "bigdecimal"] } tantivy = "0.19" time = "0.3" diff --git a/pgml-dashboard/src/api/code_editor.rs b/pgml-dashboard/src/api/code_editor.rs new file mode 100644 index 000000000..dfaac11e2 --- /dev/null +++ b/pgml-dashboard/src/api/code_editor.rs @@ -0,0 +1,285 @@ +use crate::components::code_editor::Editor; +use crate::components::turbo::TurboFrame; +use anyhow::Context; +use once_cell::sync::OnceCell; +use sailfish::TemplateOnce; +use serde::Serialize; +use sqlparser::dialect::PostgreSqlDialect; +use sqlx::{postgres::PgPoolOptions, Executor, PgPool, Row}; + +use crate::responses::ResponseOk; + +use rocket::route::Route; + +static READONLY_POOL: OnceCell = OnceCell::new(); +static ERROR: &str = + "Thanks for trying PostgresML! If you would like to run more queries, sign up for an account and create a database."; + +fn get_readonly_pool() -> PgPool { + READONLY_POOL + .get_or_init(|| { + PgPoolOptions::new() + .max_connections(1) + .idle_timeout(std::time::Duration::from_millis(60_000)) + .max_lifetime(std::time::Duration::from_millis(60_000)) + .connect_lazy(&std::env::var("CHATBOT_DATABASE_URL").expect("CHATBOT_DATABASE_URL not set")) + .expect("could not build lazy database connection") + }) + .clone() +} + +fn check_query(query: &str) -> anyhow::Result<()> { + let ast = sqlparser::parser::Parser::parse_sql(&PostgreSqlDialect {}, query)?; + + if ast.len() != 1 { + anyhow::bail!(ERROR); + } + + let query = ast + .into_iter() + .next() + .with_context(|| "impossible, ast is empty, even though we checked")?; + + match query { + sqlparser::ast::Statement::Query(query) => match *query.body { + sqlparser::ast::SetExpr::Select(_) => (), + _ => anyhow::bail!(ERROR), + }, + _ => anyhow::bail!(ERROR), + }; + + Ok(()) +} + +#[derive(FromForm, Debug)] +pub struct PlayForm { + pub query: String, +} + +pub async fn play(sql: &str) -> anyhow::Result { + check_query(sql)?; + let pool = get_readonly_pool(); + let row = sqlx::query(sql).fetch_one(&pool).await?; + let transform: serde_json::Value = row.try_get(0)?; + Ok(serde_json::to_string_pretty(&transform)?) +} + +/// Response expected by the frontend. +#[derive(Serialize)] +struct StreamResponse { + error: Option, + result: Option, +} + +impl StreamResponse { + fn from_error(error: &str) -> Self { + StreamResponse { + error: Some(error.to_string()), + result: None, + } + } + + fn from_result(result: &str) -> Self { + StreamResponse { + error: None, + result: Some(result.to_string()), + } + } +} + +impl ToString for StreamResponse { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +/// An async iterator over a PostgreSQL cursor. +#[derive(Debug)] +struct AsyncResult<'a> { + /// Open transaction. + transaction: sqlx::Transaction<'a, sqlx::Postgres>, + cursor_name: String, +} + +impl<'a> AsyncResult<'a> { + async fn from_message(message: ws::Message) -> anyhow::Result { + if let ws::Message::Text(query) = message { + let request = serde_json::from_str::(&query)?; + let query = request["sql"] + .as_str() + .context("Error sql key is required in websocket")?; + Self::new(&query).await + } else { + anyhow::bail!(ERROR) + } + } + + /// Create new AsyncResult given a query. + async fn new(query: &str) -> anyhow::Result { + let cursor_name = format!(r#""{}""#, crate::utils::random_string(12)); + + // Make sure it's a SELECT. Can't do too much damage there. + check_query(query)?; + + let pool = get_readonly_pool(); + let mut transaction = pool.begin().await?; + + let query = format!("DECLARE {} CURSOR FOR {}", cursor_name, query); + + info!( + "[stream] query: {}", + query.trim().split("\n").collect::>().join(" ") + ); + + match transaction.execute(query.as_str()).await { + Ok(_) => (), + Err(err) => { + info!("[stream] query error: {:?}", err); + anyhow::bail!(err); + } + } + + Ok(AsyncResult { + transaction, + cursor_name, + }) + } + + /// Fetch a row from the cursor, get the first column, + /// decode the value and return it as a String. + async fn next(&mut self) -> anyhow::Result> { + use serde_json::Value; + + let result = sqlx::query(format!("FETCH 1 FROM {}", self.cursor_name).as_str()) + .fetch_optional(&mut *self.transaction) + .await?; + + if let Some(row) = result { + let _column = row.columns().get(0).with_context(|| "no columns")?; + + // Handle pgml.embed() which returns an array of floating points. + if let Ok(value) = row.try_get::, _>(0) { + return Ok(Some(serde_json::to_string(&value)?)); + } + + // Anything that just returns a String, e.g. pgml.version(). + if let Ok(value) = row.try_get::(0) { + return Ok(Some(value)); + } + + // Array of strings. + if let Ok(value) = row.try_get::, _>(0) { + return Ok(Some(value.join(""))); + } + + // Integers. + if let Ok(value) = row.try_get::(0) { + return Ok(Some(value.to_string())); + } + + if let Ok(value) = row.try_get::(0) { + return Ok(Some(value.to_string())); + } + + if let Ok(value) = row.try_get::(0) { + return Ok(Some(value.to_string())); + } + + if let Ok(value) = row.try_get::(0) { + return Ok(Some(value.to_string())); + } + + // Handle functions that return JSONB, + // e.g. pgml.transform() + if let Ok(value) = row.try_get::(0) { + return Ok(Some(match value { + Value::Array(ref values) => { + let first_value = values.first(); + match first_value { + Some(Value::Object(_)) => serde_json::to_string(&value)?, + _ => values + .into_iter() + .map(|v| v.as_str().unwrap_or("").to_string()) + .collect::>() + .join(""), + } + } + + value => serde_json::to_string(&value)?, + })); + } + } + + Ok(None) + } + + async fn close(mut self) -> anyhow::Result<()> { + self.transaction + .execute(format!("CLOSE {}", self.cursor_name).as_str()) + .await?; + self.transaction.rollback().await?; + Ok(()) + } +} + +#[get("/code_editor/play/stream")] +pub async fn play_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::Stream! { ws => + for await message in ws { + let message = match message { + Ok(message) => message, + Err(_err) => continue, + }; + + let mut got_something = false; + match AsyncResult::from_message(message).await { + Ok(mut result) => { + loop { + match result.next().await { + Ok(Some(result)) => { + got_something = true; + yield ws::Message::from(StreamResponse::from_result(&result).to_string()); + } + + Err(err) => { + yield ws::Message::from(StreamResponse::from_error(&err.to_string()).to_string()); + break; + } + + Ok(None) => { + if !got_something { + yield ws::Message::from(StreamResponse::from_error(ERROR).to_string()); + } + break; + } + } + }; + + match result.close().await { + Ok(_) => (), + Err(err) => { + info!("[stream] error closing: {:?}", err); + } + }; + } + + Err(err) => { + yield ws::Message::from(StreamResponse::from_error(&err.to_string()).to_string()); + } + } + }; + } +} + +#[get("/code_editor/embed?")] +pub fn embed_editor(id: String) -> ResponseOk { + let comp = Editor::new(); + + let rsp = TurboFrame::new().set_target_id(&id).set_content(comp.into()); + + return ResponseOk(rsp.render_once().unwrap()); +} + +pub fn routes() -> Vec { + routes![play_stream, embed_editor,] +} diff --git a/pgml-dashboard/src/api/mod.rs b/pgml-dashboard/src/api/mod.rs index 8bff8d7dd..80220654b 100644 --- a/pgml-dashboard/src/api/mod.rs +++ b/pgml-dashboard/src/api/mod.rs @@ -2,11 +2,13 @@ use rocket::route::Route; pub mod chatbot; pub mod cms; +pub mod code_editor; pub mod deployment; pub fn routes() -> Vec { let mut routes = Vec::new(); routes.extend(cms::routes()); routes.extend(chatbot::routes()); + routes.extend(code_editor::routes()); routes } diff --git a/pgml-dashboard/src/components/cards/mod.rs b/pgml-dashboard/src/components/cards/mod.rs index 1356bd25d..66555b451 100644 --- a/pgml-dashboard/src/components/cards/mod.rs +++ b/pgml-dashboard/src/components/cards/mod.rs @@ -15,6 +15,10 @@ pub use newsletter_subscribe::NewsletterSubscribe; pub mod primary; pub use primary::Primary; +// src/components/cards/psychedelic +pub mod psychedelic; +pub use psychedelic::Psychedelic; + // src/components/cards/rgb pub mod rgb; pub use rgb::Rgb; diff --git a/pgml-dashboard/src/components/cards/newsletter_subscribe/template.html b/pgml-dashboard/src/components/cards/newsletter_subscribe/template.html index 4851a91a4..42737a3b4 100644 --- a/pgml-dashboard/src/components/cards/newsletter_subscribe/template.html +++ b/pgml-dashboard/src/components/cards/newsletter_subscribe/template.html @@ -1,5 +1,5 @@ <% - use pgml_components::Component; + use crate::components::cards::Psychedelic; let success_class = match success { Some(true) => "success", @@ -14,8 +14,8 @@ }; let error_icon = match success { - Some(false) => Component::from(r#"warning"#), - _ => Component::from("") + Some(false) => r#"warning"#, + _ => "" }; let email_placeholder = match &email { @@ -28,27 +28,36 @@ message } }; + + let email_val = match email { + Some(ref email) => "value=\"".to_string() + &email + "\"", + None => String::new() + }; %>
- diff --git a/pgml-dashboard/src/components/cards/psychedelic/mod.rs b/pgml-dashboard/src/components/cards/psychedelic/mod.rs new file mode 100644 index 000000000..78442b84f --- /dev/null +++ b/pgml-dashboard/src/components/cards/psychedelic/mod.rs @@ -0,0 +1,42 @@ +use pgml_components::{component, Component}; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "cards/psychedelic/template.html")] +pub struct Psychedelic { + border_only: bool, + color: String, + content: Component, +} + +impl Psychedelic { + pub fn new() -> Psychedelic { + Psychedelic { + border_only: false, + color: String::from("blue"), + content: Component::default(), + } + } + + pub fn is_border_only(mut self, border_only: bool) -> Self { + self.border_only = border_only; + self + } + + pub fn set_color_pink(mut self) -> Self { + self.color = String::from("pink"); + self + } + + pub fn set_color_blue(mut self) -> Self { + self.color = String::from("green"); + self + } + + pub fn set_content(mut self, content: Component) -> Self { + self.content = content; + self + } +} + +component!(Psychedelic); diff --git a/pgml-dashboard/src/components/cards/psychedelic/psychedelic.scss b/pgml-dashboard/src/components/cards/psychedelic/psychedelic.scss new file mode 100644 index 000000000..d144b66fa --- /dev/null +++ b/pgml-dashboard/src/components/cards/psychedelic/psychedelic.scss @@ -0,0 +1,34 @@ +div[data-controller="cards-psychedelic"] { + .psychedelic-pink-bg { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + + background-image: url("/dashboard/static/images/newsletter_subscribe_background_mobile.png"); + background-color: #{$pink}; + background-color: #{$blue}; + padding: 2px; + } + + .psychedelic-blue-bg { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + + background-image: url("/dashboard/static/images/psychedelic_blue.jpg"); + background-color: #{$blue}; + padding: 2px; + } + + .fill { + background-color: #{$mostly-black}; + } + + .psycho-as-border { + padding: 1rem; + } + + .psycho-as-background { + padding: 3rem; + } +} diff --git a/pgml-dashboard/src/components/cards/psychedelic/template.html b/pgml-dashboard/src/components/cards/psychedelic/template.html new file mode 100644 index 000000000..07cce651b --- /dev/null +++ b/pgml-dashboard/src/components/cards/psychedelic/template.html @@ -0,0 +1,8 @@ + +
+
+
+ <%+ content %> +
+
+
diff --git a/pgml-dashboard/src/components/code_block/code_block_controller.js b/pgml-dashboard/src/components/code_block/code_block_controller.js index 25b06a97e..633876ed4 100644 --- a/pgml-dashboard/src/components/code_block/code_block_controller.js +++ b/pgml-dashboard/src/components/code_block/code_block_controller.js @@ -15,7 +15,13 @@ import { editorTheme, } from "../../../static/js/utilities/code_mirror_theme"; -const buildEditorView = (target, content, languageExtension, classes) => { +const buildEditorView = ( + target, + content, + languageExtension, + classes, + editable, +) => { let editorView = new EditorView({ doc: content, extensions: [ @@ -23,7 +29,7 @@ const buildEditorView = (target, content, languageExtension, classes) => { languageExtension !== null ? languageExtension() : [], // if no language chosen do not highlight syntax EditorView.theme(editorTheme), syntaxHighlighting(HighlightStyle.define(highlightStyle)), - EditorView.contentAttributes.of({ contenteditable: false }), + EditorView.contentAttributes.of({ contenteditable: editable }), addClasses.of(classes), highlight, ], @@ -49,19 +55,22 @@ const highlight = ViewPlugin.fromClass( }, ); +// Allows for highlighting of specific lines function highlightLine(view) { let builder = new RangeSetBuilder(); let classes = view.state.facet(addClasses).shift(); - for (let { from, to } of view.visibleRanges) { - for (let pos = from; pos <= to; ) { - let lineClasses = classes.shift(); - let line = view.state.doc.lineAt(pos); - builder.add( - line.from, - line.from, - Decoration.line({ attributes: { class: lineClasses } }), - ); - pos = line.to + 1; + if (classes) { + for (let { from, to } of view.visibleRanges) { + for (let pos = from; pos <= to; ) { + let lineClasses = classes.shift(); + let line = view.state.doc.lineAt(pos); + builder.add( + line.from, + line.from, + Decoration.line({ attributes: { class: lineClasses } }), + ); + pos = line.to + 1; + } } } return builder.finish(); @@ -71,7 +80,7 @@ const addClasses = Facet.define({ combone: (values) => values, }); -const language = (element) => { +const getLanguage = (element) => { switch (element.getAttribute("language")) { case "sql": return sql; @@ -92,6 +101,15 @@ const language = (element) => { } }; +const getIsEditable = (element) => { + switch (element.getAttribute("editable")) { + case "true": + return true; + default: + return false; + } +}; + const codeBlockCallback = (element) => { let highlights = element.getElementsByClassName("highlight"); let classes = []; @@ -109,9 +127,16 @@ const codeBlockCallback = (element) => { export default class extends Controller { connect() { let [element, content, classes] = codeBlockCallback(this.element); - let lang = language(this.element); + let lang = getLanguage(this.element); + let editable = getIsEditable(this.element); + + let editor = buildEditorView(element, content, lang, classes, editable); + this.editor = editor; + this.dispatch("code-block-connected"); + } - buildEditorView(element, content, lang, classes); + getEditor() { + return this.editor; } } @@ -120,13 +145,14 @@ class CodeBlockA extends HTMLElement { constructor() { super(); - this.language = language(this); + this.language = getLanguage(this); + this.editable = getIsEditable(this); } connectedCallback() { let [element, content, classes] = codeBlockCallback(this); - buildEditorView(element, content, this.language, classes); + buildEditorView(element, content, this.language, classes, this.editable); } // component attributes diff --git a/pgml-dashboard/src/components/code_block/mod.rs b/pgml-dashboard/src/components/code_block/mod.rs index 4a68d0a7b..0dc835430 100644 --- a/pgml-dashboard/src/components/code_block/mod.rs +++ b/pgml-dashboard/src/components/code_block/mod.rs @@ -3,11 +3,36 @@ use sailfish::TemplateOnce; #[derive(TemplateOnce, Default)] #[template(path = "code_block/template.html")] -pub struct CodeBlock {} +pub struct CodeBlock { + content: String, + language: String, + editable: bool, + id: String, +} impl CodeBlock { - pub fn new() -> CodeBlock { - CodeBlock {} + pub fn new(content: &str) -> CodeBlock { + CodeBlock { + content: content.to_string(), + language: "sql".to_string(), + editable: false, + id: "code-block".to_string(), + } + } + + pub fn set_language(mut self, language: &str) -> Self { + self.language = language.to_owned(); + self + } + + pub fn set_editable(mut self, editable: bool) -> Self { + self.editable = editable; + self + } + + pub fn set_id(mut self, id: &str) -> Self { + self.id = id.to_owned(); + self } } diff --git a/pgml-dashboard/src/components/code_block/template.html b/pgml-dashboard/src/components/code_block/template.html index e69de29bb..b3b26a628 100644 --- a/pgml-dashboard/src/components/code_block/template.html +++ b/pgml-dashboard/src/components/code_block/template.html @@ -0,0 +1,8 @@ +
+ <%- content %> +
diff --git a/pgml-dashboard/src/components/code_editor/editor/editor.scss b/pgml-dashboard/src/components/code_editor/editor/editor.scss new file mode 100644 index 000000000..d9640ccfc --- /dev/null +++ b/pgml-dashboard/src/components/code_editor/editor/editor.scss @@ -0,0 +1,140 @@ +div[data-controller="code-editor-editor"] { + .text-area { + background-color: #17181a; + max-height: 388px; + overflow: auto; + + .cm-scroller { + min-height: 100px; + } + + .btn-party { + position: relative; + --bs-btn-color: #{$hp-white}; + --bs-btn-font-size: 24px; + border-radius: 0.5rem; + padding-left: 2rem; + padding-right: 2rem; + z-index: 1; + } + + .btn-party div:nth-child(1) { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: -2px; + border-radius: inherit; + background: #{$primary-gradient-main}; + } + + .btn-party div:nth-child(2) { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: inherit; + background: #{$gray-700}; + } + + .btn-party:hover div:nth-child(2) { + background: #{$primary-gradient-main}; + } + } + + div[data-code-editor-editor-target="resultStream"] { + padding-right: 5px; + } + + .lds-dual-ring { + display: inline-block; + width: 1rem; + height: 1rem; + } + .lds-dual-ring:after { + content: " "; + display: block; + width: 1rem; + height: 1rem; + margin: 0px; + border-radius: 50%; + border: 3px solid #fff; + border-color: #fff transparent #fff transparent; + animation: lds-dual-ring 1.2s linear infinite; + } + @keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + pre { + padding: 0px; + margin: 0px; + border-radius: 0; + } + + ul.dropdown-menu { + padding-bottom: 15px; + } + + .editor-header { + background-color: #{$gray-700}; + } + + .editor-header > div:first-child { + border-bottom: solid #{$gray-600} 2px; + } + + .editor-footer { + background-color: #{$gray-700}; + } + + .editor-footer code, #editor-play-result-stream, .editor-footer .loading { + height: 4rem; + overflow: auto; + display: block; + } + + input { + border: none; + } + + div[data-controller="inputs-select"] { + flex-grow: 1; + min-width: 0; + + .material-symbols-outlined { + color: #{$gray-200}; + } + } + + .btn-dropdown { + padding: 0px !important; + border: none !important; + border-radius: 0px !important; + } + + .btn-dropdown:focus, + .btn-dropdown:hover { + border: none !important; + } + + [placeholder] { + text-overflow: ellipsis; + } + + @include media-breakpoint-down(xl) { + .question-input { + justify-content: space-between; + } + input { + padding: 0px; + } + } +} diff --git a/pgml-dashboard/src/components/code_editor/editor/editor_controller.js b/pgml-dashboard/src/components/code_editor/editor/editor_controller.js new file mode 100644 index 000000000..9b2d5d54a --- /dev/null +++ b/pgml-dashboard/src/components/code_editor/editor/editor_controller.js @@ -0,0 +1,219 @@ +import { Controller } from "@hotwired/stimulus"; +import { + generateModels, + generateSql, + generateOutput, +} from "../../../../static/js/utilities/demo"; + +export default class extends Controller { + static targets = [ + "editor", + "button", + "loading", + "result", + "task", + "model", + "resultStream", + "questionInput", + ]; + + static values = { + defaultModel: String, + defaultTask: String, + runOnVisible: Boolean, + }; + + // Using an outlet is okay here since we need the exact instance of codeMirror + static outlets = ["code-block"]; + + // outlet callback not working so we listen for the + // code-block to finish setting up CodeMirror editor view. + codeBlockAvailable() { + this.editor = this.codeBlockOutlet.getEditor(); + + if (this.currentTask() !== "custom") { + this.taskChange(); + } + this.streaming = false; + this.openConnection(); + } + + openConnection() { + let protocol; + switch (window.location.protocol) { + case "http:": + protocol = "ws"; + break; + case "https:": + protocol = "wss"; + break; + default: + protocol = "ws"; + } + const url = `${protocol}://${window.location.host}/code_editor/play/stream`; + + this.socket = new WebSocket(url); + + if (this.runOnVisibleValue) { + this.socket.addEventListener("open", () => { + this.observe(); + }); + } + + this.socket.onmessage = (message) => { + let result = JSON.parse(message.data); + // We could probably clean this up + if (result.error) { + if (this.streaming) { + this.resultStreamTarget.classList.remove("d-none"); + this.resultStreamTarget.innerHTML += result.error; + } else { + this.resultTarget.classList.remove("d-none"); + this.resultTarget.innerHTML += result.error; + } + } else { + if (this.streaming) { + this.resultStreamTarget.classList.remove("d-none"); + if (result.result == "\n") { + this.resultStreamTarget.innerHTML += "

"; + } else { + this.resultStreamTarget.innerHTML += result.result; + } + this.resultStreamTarget.scrollTop = + this.resultStreamTarget.scrollHeight; + } else { + this.resultTarget.classList.remove("d-none"); + this.resultTarget.innerHTML += result.result; + } + } + this.loadingTarget.classList.add("d-none"); + this.buttonTarget.disabled = false; + }; + + this.socket.onclose = () => { + window.setTimeout(() => this.openConnection(), 500); + }; + } + + currentTask() { + return this.hasTaskTarget ? this.taskTarget.value : this.defaultTaskValue; + } + + currentModel() { + return this.hasModelTarget + ? this.modelTarget.value + : this.defaultModelValue; + } + + taskChange() { + let models = generateModels(this.currentTask()); + let elements = this.element.querySelectorAll(".hh-m .menu-item"); + let allowedElements = []; + + for (let i = 0; i < elements.length; i++) { + let element = elements[i]; + if (models.includes(element.getAttribute("data-for"))) { + element.classList.remove("d-none"); + allowedElements.push(element); + } else { + element.classList.add("d-none"); + } + } + + // Trigger a model change if the current one we have is not valid + if (!models.includes(this.currentModel())) { + allowedElements[0].firstElementChild.click(); + } else { + let transaction = this.editor.state.update({ + changes: { + from: 0, + to: this.editor.state.doc.length, + insert: generateSql(this.currentTask(), this.currentModel()), + }, + }); + this.editor.dispatch(transaction); + } + } + + modelChange() { + this.taskChange(); + } + + onSubmit(event) { + event.preventDefault(); + this.buttonTarget.disabled = true; + this.loadingTarget.classList.remove("d-none"); + this.resultTarget.classList.add("d-none"); + this.resultStreamTarget.classList.add("d-none"); + this.resultTarget.innerHTML = ""; + this.resultStreamTarget.innerHTML = ""; + + // Update code area to include the users question. + if (this.currentTask() == "embedded-query") { + let transaction = this.editor.state.update({ + changes: { + from: 0, + to: this.editor.state.doc.length, + insert: generateSql( + this.currentTask(), + this.currentModel(), + this.questionInputTarget.value, + ), + }, + }); + this.editor.dispatch(transaction); + } + + // Since db is read only, we show example result rather than sending request. + if (this.currentTask() == "create-table") { + this.resultTarget.innerHTML = generateOutput(this.currentTask()); + this.resultTarget.classList.remove("d-none"); + this.loadingTarget.classList.add("d-none"); + this.buttonTarget.disabled = false; + } else { + this.sendRequest(); + } + } + + sendRequest() { + let socketData = { + sql: this.editor.state.doc.toString(), + }; + + if (this.currentTask() == "text-generation") { + socketData.stream = true; + this.streaming = true; + } else { + this.streaming = false; + } + + this.lastSocketData = socketData; + try { + this.socket.send(JSON.stringify(socketData)); + } catch (e) { + this.openConnection(); + this.socket.send(JSON.stringify(socketData)); + } + } + + observe() { + var options = { + root: document.querySelector("#scrollArea"), + rootMargin: "0px", + threshold: 1.0, + }; + + let callback = (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.buttonTarget.click(); + this.observer.unobserve(this.element); + } + }); + }; + + this.observer = new IntersectionObserver(callback, options); + + this.observer.observe(this.element); + } +} diff --git a/pgml-dashboard/src/components/code_editor/editor/mod.rs b/pgml-dashboard/src/components/code_editor/editor/mod.rs new file mode 100644 index 000000000..5a4083493 --- /dev/null +++ b/pgml-dashboard/src/components/code_editor/editor/mod.rs @@ -0,0 +1,121 @@ +use pgml_components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "code_editor/editor/template.html")] +pub struct Editor { + show_model: bool, + show_task: bool, + show_question_input: bool, + task: String, + model: String, + btn_location: String, + btn_style: String, + is_editable: bool, + run_on_visible: bool, + content: Option, +} + +impl Editor { + pub fn new() -> Editor { + Editor { + show_model: false, + show_task: false, + show_question_input: false, + task: "text-generation".to_string(), + model: "meta-llama/Meta-Llama-3-8B-Instruct".to_string(), + btn_location: "text-area".to_string(), + btn_style: "party".to_string(), + is_editable: true, + run_on_visible: false, + content: None, + } + } + + pub fn new_embedded_query() -> Editor { + Editor { + show_model: false, + show_task: false, + show_question_input: true, + task: "embedded-query".to_string(), + model: "many".to_string(), + btn_location: "question-header".to_string(), + btn_style: "secondary".to_string(), + is_editable: false, + run_on_visible: false, + content: None, + } + } + + pub fn new_custom(content: &str) -> Editor { + Editor { + show_model: false, + show_task: false, + show_question_input: false, + task: "custom".to_string(), + model: "many".to_string(), + btn_location: "text-area".to_string(), + btn_style: "secondary".to_string(), + is_editable: true, + run_on_visible: false, + content: Some(content.to_owned()), + } + } + + pub fn set_show_model(mut self, show_model: bool) -> Self { + self.show_model = show_model; + self + } + + pub fn set_show_task(mut self, show_task: bool) -> Self { + self.show_task = show_task; + self + } + + pub fn set_show_question_input(mut self, show_question_input: bool) -> Self { + self.show_question_input = show_question_input; + self + } + + pub fn set_task(mut self, task: &str) -> Self { + self.task = task.to_owned(); + self + } + + pub fn set_model(mut self, model: &str) -> Self { + self.model = model.to_owned(); + self + } + + pub fn show_btn_in_text_area(mut self) -> Self { + self.btn_location = "text-area".to_string(); + self + } + + pub fn set_btn_style_secondary(mut self) -> Self { + self.btn_style = "secondary".to_string(); + self + } + + pub fn set_btn_style_party(mut self) -> Self { + self.btn_style = "party".to_string(); + self + } + + pub fn set_is_editable(mut self, is_editable: bool) -> Self { + self.is_editable = is_editable; + self + } + + pub fn set_run_on_visible(mut self, run_on_visible: bool) -> Self { + self.run_on_visible = run_on_visible; + self + } + + pub fn set_content(mut self, content: &str) -> Self { + self.content = Some(content.to_owned()); + self + } +} + +component!(Editor); diff --git a/pgml-dashboard/src/components/code_editor/editor/template.html b/pgml-dashboard/src/components/code_editor/editor/template.html new file mode 100644 index 000000000..5eb6631f9 --- /dev/null +++ b/pgml-dashboard/src/components/code_editor/editor/template.html @@ -0,0 +1,165 @@ +<% + use crate::components::inputs::select::Select; + use crate::components::stimulus::stimulus_target::StimulusTarget; + use crate::components::stimulus::stimulus_action::{StimulusAction, StimulusEvents}; + use crate::components::code_block::CodeBlock; + use crate::utils::random_string; + + let code_block_id = format!("code-block-{}", random_string(5)); + + let btn = if btn_style == "party" { + format!(r#" + + "#) + } else { + format!(r#" + + "#) + }; +%> + +
+
+
+
+ <% if show_task {%> +
+ + <%+ Select::new().options(vec![ + "text-generation", + "embeddings", + "summarization", + "translation", + ]) + .name("task-select") + .value_target( + StimulusTarget::new() + .controller("code-editor-editor") + .name("task") + ) + .action( + StimulusAction::new() + .controller("code-editor-editor") + .method("taskChange") + .action(StimulusEvents::Change) + ) %> +
+ <% } %> + + <% if show_model {%> +
+ + <%+ Select::new().options(vec![ + // Models are marked as C (cpu) G (gpu) + // The number is the average time it takes to run in seconds + + // text-generation + "meta-llama/Meta-Llama-3-8B-Instruct", // G + "meta-llama/Meta-Llama-3-70B-Instruct", // G + "mistralai/Mixtral-8x7B-Instruct-v0.1", // G + "mistralai/Mistral-7B-Instruct-v0.2", // G + + // Embeddings + "intfloat/e5-small-v2", + "Alibaba-NLP/gte-large-en-v1.5", + "mixedbread-ai/mxbai-embed-large-v1", + + // Translation + "google-t5/t5-base", + + // Summarization + "google/pegasus-xsum", + + ]) + .name("model-select") + .value_target( + StimulusTarget::new() + .controller("code-editor-editor") + .name("model") + ) + .action( + StimulusAction::new() + .controller("code-editor-editor").method("modelChange") + .action(StimulusEvents::Change) + ) %> +
+ <% } %> + + <% if show_question_input {%> +
+
+ + +
+ <% if btn_location == "question-header" {%> +
+ <%- btn %> +
+ <% } %> +
+ <% } %> +
+ +
+ + <%+ CodeBlock::new(&content.unwrap_or_default()) + .set_language("sql") + .set_editable(is_editable) + .set_id(&code_block_id) %> + + <% if btn_location == "text-area" {%> +
+ <%- btn %> +
+ <% } %> +
+ + +
+
+
diff --git a/pgml-dashboard/src/components/code_editor/mod.rs b/pgml-dashboard/src/components/code_editor/mod.rs new file mode 100644 index 000000000..a1b012c94 --- /dev/null +++ b/pgml-dashboard/src/components/code_editor/mod.rs @@ -0,0 +1,6 @@ +// This file is automatically generated. +// You shouldn't modify it manually. + +// src/components/code_editor/editor +pub mod editor; +pub use editor::Editor; diff --git a/pgml-dashboard/src/components/layouts/marketing/mod.rs b/pgml-dashboard/src/components/layouts/marketing/mod.rs index 228d6c3f5..ddd98a124 100644 --- a/pgml-dashboard/src/components/layouts/marketing/mod.rs +++ b/pgml-dashboard/src/components/layouts/marketing/mod.rs @@ -4,3 +4,6 @@ // src/components/layouts/marketing/base pub mod base; pub use base::Base; + +// src/components/layouts/marketing/sections +pub mod sections; diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/mod.rs b/pgml-dashboard/src/components/layouts/marketing/sections/mod.rs new file mode 100644 index 000000000..b72fd2c6e --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/mod.rs @@ -0,0 +1,5 @@ +// This file is automatically generated. +// You shouldn't modify it manually. + +// src/components/layouts/marketing/sections/three_column +pub mod three_column; diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/card.scss b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/card.scss new file mode 100644 index 000000000..ea66a3bde --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/card.scss @@ -0,0 +1,3 @@ +div[data-controller="layouts-marketing-section-three-column-card"] { + +} diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/mod.rs b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/mod.rs new file mode 100644 index 000000000..7f57bfbf0 --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/mod.rs @@ -0,0 +1,54 @@ +use pgml_components::{component, Component}; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "layouts/marketing/sections/three_column/card/template.html")] +pub struct Card { + pub title: Component, + pub icon: String, + pub color: String, + pub paragraph: Component, +} + +impl Card { + pub fn new() -> Card { + Card { + title: "title".into(), + icon: "home".into(), + color: "red".into(), + paragraph: "paragraph".into(), + } + } + + pub fn set_title(mut self, title: Component) -> Self { + self.title = title; + self + } + + pub fn set_icon(mut self, icon: &str) -> Self { + self.icon = icon.to_string(); + self + } + + pub fn set_color_red(mut self) -> Self { + self.color = "red".into(); + self + } + + pub fn set_color_orange(mut self) -> Self { + self.color = "orange".into(); + self + } + + pub fn set_color_purple(mut self) -> Self { + self.color = "purple".into(); + self + } + + pub fn set_paragraph(mut self, paragraph: Component) -> Self { + self.paragraph = paragraph; + self + } +} + +component!(Card); diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/template.html b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/template.html new file mode 100644 index 000000000..23ce1e57e --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/card/template.html @@ -0,0 +1,7 @@ +
+
+ <%- icon %> +
<%+ title %>
+

<%+ paragraph %>

+
+
diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/index.scss b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/index.scss new file mode 100644 index 000000000..3b28ed2f6 --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/index.scss @@ -0,0 +1,3 @@ +div[data-controller="layouts-marketing-section-three-column-index"] { + +} diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/mod.rs b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/mod.rs new file mode 100644 index 000000000..677b45177 --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/mod.rs @@ -0,0 +1,44 @@ +use pgml_components::{component, Component}; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "layouts/marketing/sections/three_column/index/template.html")] +pub struct Index { + title: Component, + col_1: Component, + col_2: Component, + col_3: Component, +} + +impl Index { + pub fn new() -> Index { + Index { + title: "".into(), + col_1: "".into(), + col_2: "".into(), + col_3: "".into(), + } + } + + pub fn set_title(mut self, title: Component) -> Self { + self.title = title; + self + } + + pub fn set_col_1(mut self, col_1: Component) -> Self { + self.col_1 = col_1; + self + } + + pub fn set_col_2(mut self, col_2: Component) -> Self { + self.col_2 = col_2; + self + } + + pub fn set_col_3(mut self, col_3: Component) -> Self { + self.col_3 = col_3; + self + } +} + +component!(Index); diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/template.html b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/template.html new file mode 100644 index 000000000..245a53745 --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/index/template.html @@ -0,0 +1,12 @@ +
+
+
+

<%+ title %>

+
+ <%+ col_1 %> + <%+ col_2 %> + <%+ col_3 %> +
+
+
+
diff --git a/pgml-dashboard/src/components/layouts/marketing/sections/three_column/mod.rs b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/mod.rs new file mode 100644 index 000000000..53f630a7e --- /dev/null +++ b/pgml-dashboard/src/components/layouts/marketing/sections/three_column/mod.rs @@ -0,0 +1,10 @@ +// This file is automatically generated. +// You shouldn't modify it manually. + +// src/components/layouts/marketing/sections/three_column/card +pub mod card; +pub use card::Card; + +// src/components/layouts/marketing/sections/three_column/index +pub mod index; +pub use index::Index; diff --git a/pgml-dashboard/src/components/mod.rs b/pgml-dashboard/src/components/mod.rs index d994b97cd..36c3428f4 100644 --- a/pgml-dashboard/src/components/mod.rs +++ b/pgml-dashboard/src/components/mod.rs @@ -30,6 +30,9 @@ pub mod cms; pub mod code_block; pub use code_block::CodeBlock; +// src/components/code_editor +pub mod code_editor; + // src/components/confirm_modal pub mod confirm_modal; pub use confirm_modal::ConfirmModal; @@ -128,3 +131,6 @@ pub mod tables; // src/components/test_component pub mod test_component; pub use test_component::TestComponent; + +// src/components/turbo +pub mod turbo; diff --git a/pgml-dashboard/src/components/navigation/navbar/marketing/template.html b/pgml-dashboard/src/components/navigation/navbar/marketing/template.html index f4b52deaf..1e345b9fb 100644 --- a/pgml-dashboard/src/components/navigation/navbar/marketing/template.html +++ b/pgml-dashboard/src/components/navigation/navbar/marketing/template.html @@ -11,7 +11,7 @@ ]; let solutions_tasks_links = vec![ - StaticNavLink::new("RAG".to_string(), "/test2".to_string()).icon("manage_search").disabled(true), + StaticNavLink::new("RAG".to_string(), "/rag".to_string()).icon("manage_search").disabled(true), StaticNavLink::new("NLP".to_string(), "/docs/guides/natural-language-processing".to_string()).icon("description"), StaticNavLink::new("Supervised Learning".to_string(), "/docs/guides/supervised-learning".to_string()).icon("model_training"), StaticNavLink::new("Embeddings".to_string(), "/docs/api/sql-extension/pgml.embed".to_string()).icon("subtitles"), diff --git a/pgml-dashboard/src/components/star/mod.rs b/pgml-dashboard/src/components/star/mod.rs index d84a2db45..201801ab6 100644 --- a/pgml-dashboard/src/components/star/mod.rs +++ b/pgml-dashboard/src/components/star/mod.rs @@ -16,6 +16,7 @@ static SVGS: Lazy> = Lazy::new(|| { let mut map = HashMap::new(); map.insert("green", include_str!("../../../static/images/icons/stars/green.svg")); map.insert("party", include_str!("../../../static/images/icons/stars/party.svg")); + map.insert("blue", include_str!("../../../static/images/icons/stars/blue.svg")); map.insert( "give_it_a_spin", include_str!("../../../static/images/icons/stars/give_it_a_spin.svg"), diff --git a/pgml-dashboard/src/components/turbo/mod.rs b/pgml-dashboard/src/components/turbo/mod.rs new file mode 100644 index 000000000..fe4794ab9 --- /dev/null +++ b/pgml-dashboard/src/components/turbo/mod.rs @@ -0,0 +1,6 @@ +// This file is automatically generated. +// You shouldn't modify it manually. + +// src/components/turbo/turbo_frame +pub mod turbo_frame; +pub use turbo_frame::TurboFrame; diff --git a/pgml-dashboard/src/components/turbo/turbo_frame/mod.rs b/pgml-dashboard/src/components/turbo/turbo_frame/mod.rs new file mode 100644 index 000000000..1bd376afb --- /dev/null +++ b/pgml-dashboard/src/components/turbo/turbo_frame/mod.rs @@ -0,0 +1,44 @@ +use pgml_components::{component, Component}; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default)] +#[template(path = "turbo/turbo_frame/template.html")] +pub struct TurboFrame { + src: Component, + target_id: String, + content: Option, + attributes: Vec, +} + +impl TurboFrame { + pub fn new() -> TurboFrame { + TurboFrame { + src: Component::from(""), + target_id: "".to_string(), + content: None, + attributes: vec![], + } + } + + pub fn set_src(mut self, src: Component) -> Self { + self.src = src; + self + } + + pub fn set_target_id(mut self, target_id: &str) -> Self { + self.target_id = target_id.to_string(); + self + } + + pub fn set_content(mut self, content: Component) -> Self { + self.content = Some(content); + self + } + + pub fn add_attribute(mut self, attribute: &str) -> Self { + self.attributes.push(attribute.to_string()); + self + } +} + +component!(TurboFrame); diff --git a/pgml-dashboard/src/components/turbo/turbo_frame/template.html b/pgml-dashboard/src/components/turbo/turbo_frame/template.html new file mode 100644 index 000000000..de3973b46 --- /dev/null +++ b/pgml-dashboard/src/components/turbo/turbo_frame/template.html @@ -0,0 +1,8 @@ +<% + let id_attr = format!("id={}", target_id); + let src_attr = format!("src={}", src.render_once().unwrap()); + let other_attrs = attributes.join(" "); +%> + <%- src_attr %> <%- other_attrs%>> + <%- content.unwrap_or_default().render_once().unwrap() %> + diff --git a/pgml-dashboard/src/components/turbo/turbo_frame/turbo_frame.scss b/pgml-dashboard/src/components/turbo/turbo_frame/turbo_frame.scss new file mode 100644 index 000000000..6d0dd9296 --- /dev/null +++ b/pgml-dashboard/src/components/turbo/turbo_frame/turbo_frame.scss @@ -0,0 +1 @@ +div[data-controller="turbo-turbo-frame"] {} diff --git a/pgml-dashboard/static/css/modules.scss b/pgml-dashboard/static/css/modules.scss index 2f19244a6..24571c69d 100644 --- a/pgml-dashboard/static/css/modules.scss +++ b/pgml-dashboard/static/css/modules.scss @@ -10,11 +10,13 @@ @import "../../src/components/cards/marketing/twitter_testimonial/twitter_testimonial.scss"; @import "../../src/components/cards/newsletter_subscribe/newsletter_subscribe.scss"; @import "../../src/components/cards/primary/primary.scss"; +@import "../../src/components/cards/psychedelic/psychedelic.scss"; @import "../../src/components/cards/rgb/rgb.scss"; @import "../../src/components/cards/secondary/secondary.scss"; @import "../../src/components/carousel/carousel.scss"; @import "../../src/components/chatbot/chatbot.scss"; @import "../../src/components/cms/index_link/index_link.scss"; +@import "../../src/components/code_editor/editor/editor.scss"; @import "../../src/components/dropdown/dropdown.scss"; @import "../../src/components/github_icon/github_icon.scss"; @import "../../src/components/headings/gray/gray.scss"; @@ -35,6 +37,8 @@ @import "../../src/components/inputs/text/search/search/search.scss"; @import "../../src/components/layouts/docs/docs.scss"; @import "../../src/components/layouts/marketing/base/base.scss"; +@import "../../src/components/layouts/marketing/sections/three_column/card/card.scss"; +@import "../../src/components/layouts/marketing/sections/three_column/index/index.scss"; @import "../../src/components/left_nav_menu/left_nav_menu.scss"; @import "../../src/components/loading/dots/dots.scss"; @import "../../src/components/loading/message/message.scss"; diff --git a/pgml-dashboard/static/css/scss/abstracts/variables.scss b/pgml-dashboard/static/css/scss/abstracts/variables.scss index 906cb8f00..4825500cb 100644 --- a/pgml-dashboard/static/css/scss/abstracts/variables.scss +++ b/pgml-dashboard/static/css/scss/abstracts/variables.scss @@ -141,6 +141,7 @@ $alert-notification-medium: #FF9145; $alert-notification-notice: #8CC6FF; $alert-notification-marketing: #7FFFD4; $alert-notification-high: #{$peach-shade-100}; +$mostly-black: #0D0D0E; // Background Colors diff --git a/pgml-dashboard/static/css/scss/base/_base.scss b/pgml-dashboard/static/css/scss/base/_base.scss index 80ca64b33..624974127 100644 --- a/pgml-dashboard/static/css/scss/base/_base.scss +++ b/pgml-dashboard/static/css/scss/base/_base.scss @@ -102,6 +102,11 @@ article { supported by Chrome, Edge, Opera and Firefox */ } +// because boostrap 5.3 flex-fill is broken. +.flex-1 { + flex: 1; +} + // Smooth scroll does not work in firefox and turbo. New pages will not scroll to top, so we remove smooth for Firefox. @-moz-document url-prefix() { :root { diff --git a/pgml-dashboard/static/css/scss/components/_buttons.scss b/pgml-dashboard/static/css/scss/components/_buttons.scss index 31341305f..6e6002450 100644 --- a/pgml-dashboard/static/css/scss/components/_buttons.scss +++ b/pgml-dashboard/static/css/scss/components/_buttons.scss @@ -148,7 +148,7 @@ --bs-btn-padding-y: 16px; } -.btn-secondary-web-app { +.btn-secondary-web-app, .btn-secondary-marketing { --bs-btn-padding-x: 30px; --bs-btn-padding-y: 20px; @@ -177,6 +177,20 @@ } } +.btn-secondary-marketing { + --bs-btn-padding-x: 24px; + --bs-btn-padding-y: 16px; + + --bs-btn-color: #{$gray-100}; + --bs-btn-border-color: #{$gray-100}; + + --bs-btn-hover-color: #{#{$gray-100}}; + --bs-btn-hover-border-color: #{$neon-tint-300}; + + --bs-btn-active-color: #{$gray-100}; + --bs-btn-active-border-color: #{$neon-tint-200}; +} + .btn-tertiary-web-app { color: #{$slate-tint-100}; border-bottom: 2px solid transparent; diff --git a/pgml-dashboard/static/images/icons/stars/blue.svg b/pgml-dashboard/static/images/icons/stars/blue.svg new file mode 100644 index 000000000..ec48be511 --- /dev/null +++ b/pgml-dashboard/static/images/icons/stars/blue.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pgml-dashboard/static/images/psychedelic_blue.jpg b/pgml-dashboard/static/images/psychedelic_blue.jpg new file mode 100644 index 000000000..6fe6ed5b2 Binary files /dev/null and b/pgml-dashboard/static/images/psychedelic_blue.jpg differ diff --git a/pgml-dashboard/static/js/utilities/demo.js b/pgml-dashboard/static/js/utilities/demo.js new file mode 100644 index 000000000..c31eb3344 --- /dev/null +++ b/pgml-dashboard/static/js/utilities/demo.js @@ -0,0 +1,360 @@ +export const generateSql = (task, model, userInput) => { + let input = generateInput(task, model, "sql"); + let args = generateModelArgs(task, model, "sql"); + let extraTaskArgs = generateTaskArgs(task, model, "sql"); + + if (!userInput && task == "embedded-query") { + userInput ="What is Postgres?" + } + + let argsOutput = ""; + if (args) { + argsOutput = `, + args => ${args}`; + } + + if (task == "text-generation") { + return `SELECT pgml.transform_stream( + task => '{ + "task": "${task}", + "model": "${model}"${extraTaskArgs} + }'::JSONB, + input => ${input}${argsOutput} +);` + } else if (task === "embeddings") { + return `SELECT pgml.embed( + '${model}', + 'AI is changing the world as we know it.' +);`; + } else if (task === "embedded-query") { + return `WITH embedded_query AS ( + SELECT pgml.embed ('Alibaba-NLP/gte-base-en-v1.5', '${userInput}')::vector embedding +), +context_query AS ( + SELECT chunks.chunk FROM chunks + INNER JOIN embeddings ON embeddings.chunk_id = chunks.id + ORDER BY embeddings.embedding <=> (SELECT embedding FROM embedded_query) + LIMIT 1 +) +SELECT + pgml.transform( + task => '{ + "task": "conversational", + "model": "meta-llama/Meta-Llama-3-8B-Instruct" + }'::jsonb, + inputs => ARRAY['{"role": "system", "content": "You are a friendly and helpful chatbot."}'::jsonb, replace('{"role": "user", "content": "Given the context answer the following question. ${userInput}? Context:\n\n{CONTEXT}"}', '{CONTEXT}', chunk)::jsonb], + args => '{ + "max_new_tokens": 100 + }'::jsonb + ) +FROM context_query;` + } else if (task === "create-table") { + return `CREATE TABLE IF NOT EXISTS +documents_embeddings_table ( + document text, + embedding vector(384));` + } else { + let inputs = " "; + if (Array.isArray(input)) + inputs += input.map(v => `'${v}'`).join(",\n "); + else + inputs += input; + + return `SELECT pgml.transform( + task => '{ + "task": "${task}", + "model": "${model}"${extraTaskArgs} + }'::JSONB, + inputs => ARRAY[ +${inputs} + ]${argsOutput} +);`; + + } +}; + +export const generatePython = (task, model) => { + let input = generateInput(task, model, "python"); + let modelArgs = generateModelArgs(task, model, "python"); + let taskArgs = generateTaskArgs(task, model, "python"); + + let argsOutput = ""; + if (modelArgs) { + argsOutput = `, ${modelArgs}`; + } + + if (task == "text-generation") { + return `from pgml import TransformerPipeline +pipe = TransformerPipeline("${task}", "${model}", ${taskArgs}, "postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml") +async for t in await pipe.transform_stream(${input}${argsOutput}): + print(t)`; + } else if (task === "embeddings") { + return `from pgml import Builtins +connection = Builtins("postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml") +await connection.embed('${model}', 'AI is changing the world as we know it.')` + } else { + let inputs; + if (Array.isArray(input)) + inputs = input.map(v => `"${v}"`).join(", "); + else + inputs = input; + return `from pgml import TransformerPipeline +pipe = TransformerPipeline("${task}", "${model}", ${taskArgs}"postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml") +await pipe.transform([${inputs}]${argsOutput})`; + } +} + +export const generateJavaScript = (task, model) => { + let input = generateInput(task, model, "javascript"); + let modelArgs = generateModelArgs(task, model, "javascript"); + let taskArgs = generateTaskArgs(task, model, "javascript"); + let argsOutput = "{}"; + if (modelArgs) + argsOutput = modelArgs; + + if (task == "text-generation") { + return `const pgml = require("pgml"); +const pipe = pgml.newTransformerPipeline("${task}", "${model}", ${taskArgs}"postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml"); +const it = await pipe.transform_stream(${input}, ${argsOutput}); +let result = await it.next(); +while (!result.done) { + console.log(result.value); + result = await it.next(); +}`; + } else if (task === "embeddings") { + return `const pgml = require("pgml"); +const connection = pgml.newBuiltins("postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml"); +let embedding = await connection.embed('${model}', 'AI is changing the world as we know it!'); +` + } else { + let inputs; + if (Array.isArray(input)) + inputs = input.map(v => `"${v}"`).join(", "); + else + inputs = input; + return `const pgml = require("pgml"); +const pipe = pgml.newTransformerPipeline("${task}", "${model}", ${taskArgs}"postgres://pg:ml@sql.cloud.postgresml.org:6432/pgml"); +await pipe.transform([${inputs}], ${argsOutput});`; + } +} + +const generateTaskArgs = (task, model, language) => { + if (model == "bert-base-uncased") { + if (language == "sql") + return `, + "trust_remote_code": true`; + else if (language == "python") + return `{"trust_remote_code": True}, ` + else if (language == "javascript") + return `{trust_remote_code: true}, ` + } else if (model == "lmsys/fastchat-t5-3b-v1.0" || model == "SamLowe/roberta-base-go_emotions") { + if (language == "sql") + return `, + "device_map": "auto"`; + else if (language == "python") + return `{"device_map": "auto"}, ` + else if (language == "javascript") + return `{device_map: "auto"}, ` + } + + if (task == "summarization") { + if (language == "sql") + return `` + else if (language == "python") + return `{}, ` + else if (language == "javascript") + return `{}, ` + } + + if (task == "text-generation") { + if (language == "sql") { + return `` + } else if (language == "python") + return `{}` + else if (language == "javascript") + return `{}, ` + } + + if (language == "python" || language == "javascript") + return "{}, " + + return "" +} + +const generateModelArgs = (task, model, language) => { + switch (model) { + case "sileod/deberta-v3-base-tasksource-nli": + case "facebook/bart-large-mnli": + if (language == "sql") { + return `'{ + "candidate_labels": ["amazing", "not amazing"] + }'::JSONB`; + } else if (language == "python") { + return `{"candidate_labels": ["amazing", "not amazing"]}`; + } else if (language == "javascript") { + return `{candidate_labels: ["amazing", "not amazing"]}`; + } + case "mDeBERTa-v3-base-mnli-xnli": + if (language == "sql") { + return `'{ + "candidate_labels": ["politics", "economy", "entertainment", "environment"] + }'::JSONB`; + } else if (language == "python") { + return `{"candidate_labels": ["politics", "economy", "entertainment", "environment"]}`; + } else if (language == "javascript") { + return `{candidate_labels: ["politics", "economy", "entertainment", "environment"]}`; + } + } + + if (task == "text-generation") { + if (language == "sql") { + return `'{ + "max_new_tokens": 100 + }'::JSONB`; + } else if (language == "python") { + return `{"max_new_tokens": 100}`; + } else if (language == "javascript") { + return `{max_new_tokens: 100}`; + } + } + + if (language == "python" || language == "javascript") + return "{}" + + return ""; +}; + +export const generateModels = (task) => { + switch (task) { + case "embeddings": + return [ + "intfloat/e5-small-v2", + "Alibaba-NLP/gte-large-en-v1.5", + "mixedbread-ai/mxbai-embed-large-v1", + ]; + case "text-classification": + return [ + "distilbert-base-uncased-finetuned-sst-2-english", + "SamLowe/roberta-base-go_emotions", + "ProsusAI/finbert", + ]; + case "token-classification": + return [ + "dslim/bert-base-NER", + "vblagoje/bert-english-uncased-finetuned-pos", + "d4data/biomedical-ner-all", + ]; + case "translation": + return ["google-t5/t5-base"]; + case "summarization": + return [ + "google/pegasus-xsum", + ]; + case "question-answering": + return [ + "deepset/roberta-base-squad2", + "distilbert-base-cased-distilled-squad", + "distilbert-base-uncased-distilled-squad", + ]; + case "text-generation": + return [ + "meta-llama/Meta-Llama-3-8B-Instruct", + "meta-llama/Meta-Llama-3-70B-Instruct", + "mistralai/Mixtral-8x7B-Instruct-v0.1", + "mistralai/Mistral-7B-Instruct-v0.2", + ]; + case "text2text-generation": + return [ + "google/flan-t5-base", + "lmsys/fastchat-t5-3b-v1.0", + "grammarly/coedit-large", + ]; + case "fill-mask": + return ["bert-base-uncased", "distilbert-base-uncased", "roberta-base"]; + case "zero-shot-classification": + return [ + "facebook/bart-large-mnli", + "sileod/deberta-v3-base-tasksource-nli", + ]; + case "embedded-query": + return [ + "many" + ] + case "create-table": + return [ + "none" + ] + } +}; + +const generateInput = (task, model, language) => { + let sd; + if (language == "sql") + sd = "'" + else + sd = '"' + + if (task == "text-classification") { + if (model == "ProsusAI/finbert") + return ["Stocks rallied and the British pound gained", "Stocks fell and the British pound lost"]; + return ["I love how amazingly simple ML has become!", "I hate doing mundane and thankless tasks."]; + + } else if (task == "zero-shot-classification") { + return `${sd}PostgresML is an absolute game changer!${sd}`; + + } else if (task == "token-classification") { + if (model == "d4data/biomedical-ner-all") + return `${sd}CASE: A 28-year-old previously healthy man presented with a 6-week history of palpitations. The symptoms occurred during rest, 2–3 times per week, lasted up to 30 minutes at a time and were associated with dyspnea. Except for a grade 2/6 holosystolic tricuspid regurgitation murmur (best heard at the left sternal border with inspiratory accentuation), physical examination yielded unremarkable findings.${sd}`; + return `${sd}PostgresML - the future of machine learning${sd}`; + + } else if (task == "summarization") { + return `${sd}PostgresML is the future of GPU accelerated machine learning! It is the best tool for doing machine learning in the database.${sd}`; + + } else if (task == "translation") { + return `${sd}translate English to French: You know Postgres. Now you know machine learning.${sd}`; + + } else if (task == "question-answering") { + if (language == "sql") { + return `'{ + "question": "Is PostgresML the best?", + "context": "PostgresML is the best!" + }'`; + } else if (language == "python") { + return `'{"question": "Is PostgresML the best?", "context": "PostgresML is the best!"}'` + } else if (language == "javascript") { + return `'{"question": "Is PostgresML the best?", "context": "PostgresML is the best!"}'` + } + + } else if (task == "text2text-generation") { + if (model == "grammarly/coedit-large") + return `${sd}Make this text coherent: PostgresML is the best. It provides super fast machine learning in the database.${sd}`; + return `${sd}translate from English to French: Welcome to the future!${sd}`; + + } else if (task == "fill-mask") { + if (model == "roberta-base") { + return `${sd}Paris is the of France.${sd}`; + } + return `${sd}Paris is the [MASK] of France.${sd}`; + } + + else if (task == "text-generation") { + return `${sd}AI is going to${sd}`; + } + + else if (task === "embedding-query") { + return `A complete RAG pipeline in a single line of SQL. It does embedding, retrieval and text generation all-in-one SQL query.`; + } + + return `${sd}AI is going to${sd}`; +}; + +export const generateOutput = (task) => { + switch (task) { + case "create-table": + return `Table "public.document_embeddings_table" + Column | Type | Collation | Nullable | Default +-----------+-------------+-----------+----------+--------- + document | text | | | + embedding | vector(384) | | | ` + } +}; diff --git a/pgml-dashboard/templates/content/playground.html b/pgml-dashboard/templates/content/playground.html index a47989a60..086bac8ae 100644 --- a/pgml-dashboard/templates/content/playground.html +++ b/pgml-dashboard/templates/content/playground.html @@ -14,6 +14,8 @@ use crate::components::pagination::Pagination; use crate::components::inputs::{range::Range, RangeGroupPricingCalc}; use crate::components::tables::ServerlessModels; +use crate::components::cards::Rgb; +use crate::components::cards::Psychedelic; %>
@@ -322,7 +324,21 @@

Inputs

<%+ ServerlessModels::new() %>
- - Getting models - +
+
RGB card
+ <%+ Rgb::new("hi".into()).active() %> +
+
+
Psychedelic card
+ <%+ Psychedelic::new() %> + <%+ Psychedelic::new().is_border_only(true) %> + <%+ Psychedelic::new().set_color_pink() %> + <%+ Psychedelic::new().set_color_pink().is_border_only(true) %> +
+ +
+ + Loading our current pricing model... + +