From c56312cdf328b2e01495e5ce1881ac72986af485 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 17 Jan 2025 12:00:09 -0500 Subject: [PATCH 01/50] =?UTF-8?q?init;=20=F0=9F=90=99=20assistant=20toggle?= =?UTF-8?q?=20button=20created?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/Settings.re | 22 ++- src/haz3lweb/app/assistant/AssistantModel.re | 17 ++ src/haz3lweb/app/inspector/CursorInspector.re | 21 ++ src/haz3lweb/www/style/assistant.css | 182 ++++++++++++++++++ 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 src/haz3lweb/app/assistant/AssistantModel.re create mode 100644 src/haz3lweb/www/style/assistant.css diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index d9d369dfe3..12afff23c2 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -11,6 +11,7 @@ module Model = { instructor_mode: bool, benchmark: bool, explainThis: ExplainThisModel.Settings.t, + assistant: AssistantModel.Settings.t, }; let init = { @@ -42,6 +43,10 @@ module Model = { show_feedback: false, highlight: NoHighlight, }, + assistant: { + show: false, + human: false, + }, }; let fix_instructor_mode = settings => @@ -91,7 +96,8 @@ module Update = { | ContextInspector | InstructorMode | Evaluation(evaluation) - | ExplainThis(ExplainThisModel.Settings.action); + | ExplainThis(ExplainThisModel.Settings.action) + | Assistant(AssistantModel.Settings.action); let update = (action, settings: Model.t): Updated.t(Model.t) => { ( @@ -200,6 +206,20 @@ module Update = { }; let explainThis = {...settings.explainThis, highlight}; {...settings, explainThis}; + | Assistant(ToggleShow) => { + ...settings, + assistant: { + ...settings.assistant, + show: !settings.assistant.show, + }, + } + | Assistant(ToggleHuman) => { + ...settings, + assistant: { + ...settings.assistant, + show: !settings.assistant.human, + }, + } | Benchmark => {...settings, benchmark: !settings.benchmark} | Captions => {...settings, captions: !settings.captions} | SecondaryIcons => { diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re new file mode 100644 index 0000000000..8cfbefc5b5 --- /dev/null +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -0,0 +1,17 @@ +module Sexp = Sexplib.Sexp; +open Haz3lcore; +open ExplainThisForm; +open Util; + +module Settings = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + show: bool, + human: bool, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type action = + | ToggleShow + | ToggleHuman; +}; diff --git a/src/haz3lweb/app/inspector/CursorInspector.re b/src/haz3lweb/app/inspector/CursorInspector.re index b33aec0586..2b5a7e340e 100644 --- a/src/haz3lweb/app/inspector/CursorInspector.re +++ b/src/haz3lweb/app/inspector/CursorInspector.re @@ -34,6 +34,26 @@ let explain_this_toggle = (~globals: Globals.t): Node.t => { ); }; +let assistant_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Toggle LLM-assistant coder"; + let toggle_assistant = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(ToggleShow))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["assisstant-button"])], + [ + Widgets.toggle( + ~tooltip, + "?", + globals.settings.assistant.show, + toggle_assistant, + ), + ], + ); +}; + let cls_view = (ci: Info.t): Node.t => div( ~attrs=[clss(["syntax-class"])], @@ -61,6 +81,7 @@ let term_view = (~globals: Globals.t, ci) => { ctx_toggle(~globals), div(~attrs=[clss(["term-tag"])], [text(sort)]), explain_this_toggle(~globals), + assistant_toggle(~globals), cls_view(ci), ], ); diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css new file mode 100644 index 0000000000..346e98ec15 --- /dev/null +++ b/src/haz3lweb/www/style/assistant.css @@ -0,0 +1,182 @@ +#explain-this { + display: flex; + width: 20em; + padding: 0.4em; + flex-direction: column; + gap: 1em; + } + + #explain-this #examples { + display: flex; + flex-direction: column; + gap: 0.75em; + } + + #explain-this .example { + display: flex; + flex-direction: column; + gap: 0.75em; + } + + #explain-this .example + .example { + border-top: 1px dotted var(--BR2); + padding-top: 0.75em; + } + + #explain-this .example .cell { + width: auto; + background-color: var(--T2); + } + + #explain-this .example .cell-result { + border-radius: 0 0 0.4em 0.4em; + } + + #explain-this .example .cell-result .toggle-switch { + display: none; + } + + #explain-this .explanation { + white-space: normal; + } + + #explain-this .specificity-options-menu { + transform-origin: top; + transform: scaleY(0); + transition: transform 0.07s ease; + background-color: var(--menu-bkg); + border: 0.6px solid var(--menu-outline); + border-top: none; + } + + #explain-this .specificity-options-menu .selected { + filter: brightness(0.9); + } + + #assistant-button { + text-transform: none; + } + + #explain-this .header { + display: flex; + justify-content: flex-start; + gap: 0.75em; + } + + #explain-this .header .close { + position: relative; + cursor: pointer; + display: flex; + margin-left: auto; + align-items: flex-start; + } + + #explain-this .header .close svg { + fill: var(--BR2); + } + + #explain-this .header .close:hover svg { + animation: wobble 0.6s ease 0s 1 normal forwards; + transform: scale(130%); + filter: brightness(1.1); + } + + #explain-this .section { + display: flex; + flex-direction: column; + padding: 0 0.75em; + gap: 0.5em; + } + + #explain-this .section .explanation-contents .code { + font-family: var(--code-font); + } + + #explain-this .section .section-title { + text-transform: uppercase; + font-weight: bold; + color: var(--ui-header-text); + font-size: 0.8em; + } + + #explain-this .section .explanation-contents { + white-space: normal; + } + + #explain-this div.expandable-target { + display: flex; + cursor: pointer; + z-index: var(--explainthis-expander-z); + } + + #explain-this .clickable { + cursor: pointer; + } + + /* https://css-tricks.com/snippets/css/css-triangle/ */ + #explain-this .arrow { + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid var(--explain-this-expander); + } + #explain-this div.expandable-target:hover .arrow, + #explain-this .arrow:hover { + border-top: 6px solid black; + } + + #explain-this svg.expandable { + fill: none; + stroke-dasharray: 3, 3; + stroke: var(--explain-this-expander); + z-index: var(--err-hole-z); + } + + #explain-this svg.expandable path { + vector-effect: non-scaling-stroke; + } + + #explain-this .feedback { + display: flex; + padding-top: 10px; + } + + #explain-this .feedback .message { + font-size: smaller; + font-style: italic; + } + + #explain-this .feedback .option { + margin-left: 5px; + cursor: pointer; + } + + #explain-this .feedback .option.active { + background-color: var(--BR2); + } + + .highlight-a { + background-color: var(--highlight-a); + } + + .highlight-code-a { + fill: var(--highlight-a); + } + + .highlight-b { + background-color: var(--highlight-b); + } + + .highlight-code-b { + fill: var(--highlight-b); + } + + .highlight-c { + background-color: var(--highlight-c); + } + + .highlight-code-c { + fill: var(--highlight-c); + } + \ No newline at end of file From 9bee1e9ec278d85da3cf258567348a43c12b2fe1 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 17 Jan 2025 13:30:36 -0500 Subject: [PATCH 02/50] =?UTF-8?q?=F0=9F=8C=92=20added=20sidebar=20for=20as?= =?UTF-8?q?sistant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/app/assistant/Assistant.re | 33 ++++ src/haz3lweb/app/assistant/AssistantModel.re | 3 +- src/haz3lweb/view/Page.re | 39 +++- src/haz3lweb/www/style.css | 7 + src/haz3lweb/www/style/assistant.css | 188 ++----------------- src/haz3lweb/www/style/explainthis.css | 5 +- 6 files changed, 92 insertions(+), 183 deletions(-) create mode 100644 src/haz3lweb/app/assistant/Assistant.re diff --git a/src/haz3lweb/app/assistant/Assistant.re b/src/haz3lweb/app/assistant/Assistant.re new file mode 100644 index 0000000000..77bbce3b28 --- /dev/null +++ b/src/haz3lweb/app/assistant/Assistant.re @@ -0,0 +1,33 @@ +open Virtual_dom.Vdom; +open Node; +open Util.Web; +open Util; +open Haz3lcore; + +/* + let human_button = + Widgets.button_named( + Icons.export, + _ => inject(ExportModule), + ~tooltip="Export Exercise Module", + ); + + let assistant_button = + Widgets.button_named( + Icons.export, + _ => inject(ExportModule), + ~tooltip="Export Exercise Module", + ); + */ + +let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { + div( + ~attrs=[Attr.id("side-bar")], + [ + div( + ~attrs=[Attr.id("assistant")], + [div(~attrs=[clss(["assistant-title"])], [text("Assistant")])], + ), + ], + ); +}; diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 8cfbefc5b5..caa6752c17 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -1,8 +1,9 @@ module Sexp = Sexplib.Sexp; open Haz3lcore; -open ExplainThisForm; open Util; +type t = {none: bool}; + module Settings = { [@deriving (show({with_path: false}), sexp, yojson)] type t = { diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index a55487c0fe..01bca28432 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -37,6 +37,15 @@ module Store = { explain_this, selection: Editors.Selection.default_selection(editors), }; + /* Russ todo: + let assistant = Assist.Store.load(); + { + editors, + globals, + explain_this, + selection: Editors.Selection.default_selection(editors), + }; + */ }; let save = (m: Model.t): unit => { @@ -46,6 +55,7 @@ module Store = { ); Globals.Model.save(m.globals); ExplainThisModel.Store.save(m.explain_this); + /* Russ todo: Assistant.Store.save(); */ }; }; @@ -466,15 +476,32 @@ module View = { ~inject=a => inject(Editors(a)), cursor, ); - let sidebar = + let explainThisSidebar = globals.settings.explainThis.show && globals.settings.core.statics - ? ExplainThis.view( + ? { + ExplainThis.view( ~globals, - ~inject=a => inject(ExplainThis(a)), + ~inject=action => inject(ExplainThis(action)), ~explainThisModel, cursor.info, - ) - : div([]); + ); + } + : { + div([]); + }; + let assistantSidebar = + globals.settings.assistant.show && globals.settings.core.statics + ? { + Assistant.view(~globals, ~inject=_ => Ui_effect.Ignore); + } + : { + div([]); + }; + let sidebars = + div( + ~attrs=[Attr.id("sidebars")], + [explainThisSidebar, assistantSidebar], + ); let editors_view = Editors.View.view( ~globals, @@ -494,7 +521,7 @@ module View = { ], editors_view, ), - sidebar, + sidebars, bottom_bar, ContextInspector.view(~globals, cursor.info), ]; diff --git a/src/haz3lweb/www/style.css b/src/haz3lweb/www/style.css index d70e70a422..39fe1408d5 100644 --- a/src/haz3lweb/www/style.css +++ b/src/haz3lweb/www/style.css @@ -9,6 +9,7 @@ @import "style/type-display.css"; @import "style/projectors-panel.css"; @import "style/explainthis.css"; +@import "style/assistant.css"; @import "style/exercise-mode.css"; @import "style/cell.css"; @import "style/dynamics.css"; @@ -106,6 +107,12 @@ select:hover { scrollbar-color: var(--main-scroll-thumb) var(--NONE); } +#sidebars { + display: flex; + justify-content: space-between; + width: 100%; +} + #top-bar { grid-row: 1 / span 1; grid-column: 1 / span 2; diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 346e98ec15..a1ad646a52 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -1,182 +1,20 @@ -#explain-this { +#assistant { display: flex; + flex: 1; width: 20em; - padding: 0.4em; + padding: 1em; flex-direction: column; + overflow-y: auto; gap: 1em; - } - - #explain-this #examples { - display: flex; - flex-direction: column; - gap: 0.75em; - } - - #explain-this .example { - display: flex; - flex-direction: column; - gap: 0.75em; - } - - #explain-this .example + .example { - border-top: 1px dotted var(--BR2); - padding-top: 0.75em; - } - - #explain-this .example .cell { - width: auto; - background-color: var(--T2); - } - - #explain-this .example .cell-result { - border-radius: 0 0 0.4em 0.4em; - } - - #explain-this .example .cell-result .toggle-switch { - display: none; - } - - #explain-this .explanation { - white-space: normal; - } - - #explain-this .specificity-options-menu { - transform-origin: top; - transform: scaleY(0); - transition: transform 0.07s ease; - background-color: var(--menu-bkg); - border: 0.6px solid var(--menu-outline); - border-top: none; - } - - #explain-this .specificity-options-menu .selected { - filter: brightness(0.9); - } - - #assistant-button { +} + +#assistant .assistant-button { + text-transform: none; +} + +#assistant .assistant-title { text-transform: none; - } - - #explain-this .header { - display: flex; - justify-content: flex-start; - gap: 0.75em; - } - - #explain-this .header .close { - position: relative; - cursor: pointer; - display: flex; - margin-left: auto; - align-items: flex-start; - } - - #explain-this .header .close svg { - fill: var(--BR2); - } - - #explain-this .header .close:hover svg { - animation: wobble 0.6s ease 0s 1 normal forwards; - transform: scale(130%); - filter: brightness(1.1); - } - - #explain-this .section { - display: flex; - flex-direction: column; - padding: 0 0.75em; - gap: 0.5em; - } - - #explain-this .section .explanation-contents .code { - font-family: var(--code-font); - } - - #explain-this .section .section-title { - text-transform: uppercase; font-weight: bold; color: var(--ui-header-text); - font-size: 0.8em; - } - - #explain-this .section .explanation-contents { - white-space: normal; - } - - #explain-this div.expandable-target { - display: flex; - cursor: pointer; - z-index: var(--explainthis-expander-z); - } - - #explain-this .clickable { - cursor: pointer; - } - - /* https://css-tricks.com/snippets/css/css-triangle/ */ - #explain-this .arrow { - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 6px solid var(--explain-this-expander); - } - #explain-this div.expandable-target:hover .arrow, - #explain-this .arrow:hover { - border-top: 6px solid black; - } - - #explain-this svg.expandable { - fill: none; - stroke-dasharray: 3, 3; - stroke: var(--explain-this-expander); - z-index: var(--err-hole-z); - } - - #explain-this svg.expandable path { - vector-effect: non-scaling-stroke; - } - - #explain-this .feedback { - display: flex; - padding-top: 10px; - } - - #explain-this .feedback .message { - font-size: smaller; - font-style: italic; - } - - #explain-this .feedback .option { - margin-left: 5px; - cursor: pointer; - } - - #explain-this .feedback .option.active { - background-color: var(--BR2); - } - - .highlight-a { - background-color: var(--highlight-a); - } - - .highlight-code-a { - fill: var(--highlight-a); - } - - .highlight-b { - background-color: var(--highlight-b); - } - - .highlight-code-b { - fill: var(--highlight-b); - } - - .highlight-c { - background-color: var(--highlight-c); - } - - .highlight-code-c { - fill: var(--highlight-c); - } - \ No newline at end of file + font-size: 1.2em; +} \ No newline at end of file diff --git a/src/haz3lweb/www/style/explainthis.css b/src/haz3lweb/www/style/explainthis.css index 9399b5305b..7ac7e1d48d 100644 --- a/src/haz3lweb/www/style/explainthis.css +++ b/src/haz3lweb/www/style/explainthis.css @@ -1,9 +1,12 @@ #explain-this { display: flex; + flex: 1; width: 20em; - padding: 0.4em; + padding: 1em; flex-direction: column; + overflow-y: auto; gap: 1em; + margin-right: 1em; } #explain-this #examples { From 70c90c2c6fc5cb01fe31cd6595bde73d4da751a1 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 18 Jan 2025 17:28:17 -0500 Subject: [PATCH 03/50] =?UTF-8?q?=F0=9F=8C=92=20made=20sidebar=20persist,?= =?UTF-8?q?=20VSCode=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/Settings.re | 42 +++++++--- src/haz3lweb/app/assistant/Assistant.re | 23 ++---- src/haz3lweb/app/assistant/AssistantModel.re | 20 +++-- src/haz3lweb/app/common/Icons.re | 30 ++++++++ src/haz3lweb/app/explainthis/ExplainThis.re | 13 +--- .../app/explainthis/ExplainThisModel.re | 4 +- src/haz3lweb/app/input/Shortcut.re | 4 +- src/haz3lweb/app/inspector/CursorInspector.re | 77 ++++++++++++------- src/haz3lweb/app/sidebar/Sidebar.re | 55 +++++++++++++ src/haz3lweb/app/sidebar/SidebarModel.re | 21 +++++ src/haz3lweb/view/NutMenu.re | 7 +- src/haz3lweb/view/Page.re | 44 +++++------ src/haz3lweb/www/style.css | 4 +- src/haz3lweb/www/style/sidebar.css | 57 ++++++++++++++ 14 files changed, 291 insertions(+), 110 deletions(-) create mode 100644 src/haz3lweb/app/sidebar/Sidebar.re create mode 100644 src/haz3lweb/app/sidebar/SidebarModel.re create mode 100644 src/haz3lweb/www/style/sidebar.css diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index 12afff23c2..d49f417e13 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -12,6 +12,7 @@ module Model = { benchmark: bool, explainThis: ExplainThisModel.Settings.t, assistant: AssistantModel.Settings.t, + sidebar: SidebarModel.Settings.t, }; let init = { @@ -39,13 +40,16 @@ module Model = { instructor_mode: true, benchmark: false, explainThis: { - show: true, show_feedback: false, highlight: NoHighlight, }, assistant: { - show: false, - human: false, + llm: Human, + lsp: Human, + }, + sidebar: { + window: LanguageDocumentation, + show: true, }, }; @@ -96,6 +100,7 @@ module Update = { | ContextInspector | InstructorMode | Evaluation(evaluation) + | Sidebar(SidebarModel.Settings.action) | ExplainThis(ExplainThisModel.Settings.action) | Assistant(AssistantModel.Settings.action); @@ -180,11 +185,18 @@ module Update = { evaluation, }, }; - | ExplainThis(ToggleShow) => { + | Sidebar(ToggleShow) => { ...settings, - explainThis: { - ...settings.explainThis, - show: !settings.explainThis.show, + sidebar: { + ...settings.sidebar, + show: !settings.sidebar.show, + }, + } + | Sidebar(SwitchWindow(windowToSwitchTo)) => { + ...settings, + sidebar: { + ...settings.sidebar, + window: windowToSwitchTo, }, } | ExplainThis(ToggleShowFeedback) => { @@ -206,18 +218,26 @@ module Update = { }; let explainThis = {...settings.explainThis, highlight}; {...settings, explainThis}; - | Assistant(ToggleShow) => { + | Assistant(ToggleLLM) => { ...settings, assistant: { ...settings.assistant, - show: !settings.assistant.show, + llm: + switch (settings.assistant.llm) { + | Agent => Human + | Human => Agent + }, }, } - | Assistant(ToggleHuman) => { + | Assistant(ToggleLSP) => { ...settings, assistant: { ...settings.assistant, - show: !settings.assistant.human, + lsp: + switch (settings.assistant.lsp) { + | LanguageServer => Human + | Human => LanguageServer + }, }, } | Benchmark => {...settings, benchmark: !settings.benchmark} diff --git a/src/haz3lweb/app/assistant/Assistant.re b/src/haz3lweb/app/assistant/Assistant.re index 77bbce3b28..fc333e7f4b 100644 --- a/src/haz3lweb/app/assistant/Assistant.re +++ b/src/haz3lweb/app/assistant/Assistant.re @@ -4,29 +4,18 @@ open Util.Web; open Util; open Haz3lcore; -/* - let human_button = - Widgets.button_named( - Icons.export, - _ => inject(ExportModule), - ~tooltip="Export Exercise Module", - ); - - let assistant_button = - Widgets.button_named( - Icons.export, - _ => inject(ExportModule), - ~tooltip="Export Exercise Module", - ); - */ - let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { div( ~attrs=[Attr.id("side-bar")], [ div( ~attrs=[Attr.id("assistant")], - [div(~attrs=[clss(["assistant-title"])], [text("Assistant")])], + [ + div( + ~attrs=[clss(["assistant-title"])], + [text("Assistant Chat")], + ), + ], ), ], ); diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index caa6752c17..72f0f121d9 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -2,17 +2,25 @@ module Sexp = Sexplib.Sexp; open Haz3lcore; open Util; -type t = {none: bool}; - module Settings = { + [@deriving (show({with_path: false}), sexp, yojson)] + type llm_input = + | Agent + | Human; + + [@deriving (show({with_path: false}), sexp, yojson)] + type lsp_input = + | LanguageServer + | Human; + [@deriving (show({with_path: false}), sexp, yojson)] type t = { - show: bool, - human: bool, + llm: llm_input, + lsp: lsp_input, }; [@deriving (show({with_path: false}), sexp, yojson)] type action = - | ToggleShow - | ToggleHuman; + | ToggleLLM + | ToggleLSP; }; diff --git a/src/haz3lweb/app/common/Icons.re b/src/haz3lweb/app/common/Icons.re index 52d4e130db..741ddbd66c 100644 --- a/src/haz3lweb/app/common/Icons.re +++ b/src/haz3lweb/app/common/Icons.re @@ -231,3 +231,33 @@ let command_palette_sparkle = "m554.76 426.6c6.5195-23.285 24.715-41.48 48-48-23.297-6.5-41.5-24.707-48-48-6.5 23.293-24.707 41.5-48 48 23.281 6.5195 41.477 24.715 48 48z", ], ); + +let assistant = + simple_icon( + ~view="0 0 512 512", + [ + "M440.86,303.607c-27.106,0-59.31-6.311-68.409-24.022c-5.41-10.534,0.128-22.059,5.728-29.872 + c21.037-29.36,32.158-62.108,32.158-94.703C410.336,69.538,341.101,0,255.999,0S101.662,69.538,101.662,155.01 + c0,32.595,11.12,65.343,32.158,94.703c5.598,7.813,11.138,19.339,5.728,29.872c-9.099,17.712-41.303,24.022-68.409,24.022 + c-30.01,0-54.424,24.497-54.424,54.609s24.415,54.609,54.424,54.609c13.878,0,32.714-3.057,49.694-7.208 + c-3.563,2.705-7.187,5.417-10.638,7.91c-21.305,15.39-28.094,40.131-17.718,64.568c6.132,14.441,18.071,25.704,32.76,30.902 + c13.514,4.781,27.708,3.827,39.96-2.695c35.939-19.129,59.199-51.213,76.181-74.639c4.617-6.369,10.521-14.513,14.62-19.001 + c4.099,4.489,10.003,12.633,14.621,19.001c16.983,23.426,40.242,55.509,76.181,74.639c7.114,3.786,14.882,5.698,22.793,5.698 + c5.713,0,11.5-0.997,17.168-3.002c14.688-5.197,26.629-16.46,32.76-30.902c10.375-24.438,3.587-49.178-17.717-64.568 + c-3.453-2.493-7.075-5.206-10.639-7.91c16.979,4.15,35.818,7.208,49.694,7.208c30.01,0,54.424-24.497,54.424-54.609 + C495.285,328.106,470.87,303.607,440.86,303.607z M204.312,193.512c-11.237,0-20.381-9.291-20.381-20.753 + c0-11.463,9.144-20.747,20.381-20.747c11.251,0,20.394,9.284,20.394,20.747C224.706,184.22,215.564,193.512,204.312,193.512z + M307.532,193.666c-11.346,0-20.55-9.36-20.55-20.908c0-11.547,9.204-20.908,20.55-20.908c11.332,0,20.535,9.36,20.535,20.908 + C328.067,184.306,318.865,193.666,307.532,193.666z", + ], + ); + +let explain_this = + simple_icon( + ~view="0 0 1024 1024", + [ + "M263.508 346.359c0-11.782 9.551-21.333 21.333-21.333h303.012c11.78 0 21.333 9.551 21.333 21.333s-9.553 21.333-21.333 21.333H284.841c-11.782 0-21.333-9.551-21.333-21.333zm21.333 92.937c-11.782 0-21.333 9.553-21.333 21.333 0 11.785 9.551 21.333 21.333 21.333h303.012c11.78 0 21.333-9.549 21.333-21.333 0-11.78-9.553-21.333-21.333-21.333H284.841zm-21.333 135.599c0-11.78 9.551-21.333 21.333-21.333h303.012c11.78 0 21.333 9.553 21.333 21.333 0 11.785-9.553 21.333-21.333 21.333H284.841c-11.782 0-21.333-9.549-21.333-21.333zm21.333 92.535c-11.782 0-21.333 9.553-21.333 21.333 0 11.785 9.551 21.333 21.333 21.333h303.012c11.78 0 21.333-9.549 21.333-21.333 0-11.78-9.553-21.333-21.333-21.333H284.841z", + "M325.731 43.151h15.654c1.387-.283 2.823-.432 4.294-.432s2.907.149 4.294.432H654.22c37.875 0 68.74 30.919 68.74 68.78v649.225c0 37.858-30.865 68.779-68.74 68.779H218.073c-37.873 0-68.741-30.921-68.741-68.779V212.754c0-.922.058-1.831.172-2.722.466-11.074 4.843-22.22 13.986-31.371L285.747 56.306c11.501-11.236 26.231-15.109 39.984-13.155zM193.673 208.819L315.626 86.765c.943-.899 1.808-1.238 2.577-1.366.895-.149 1.968-.049 3.028.39 1.055.437 1.833 1.1 2.312 1.78.366.52.73 1.278.803 2.512v70.051c0 .256.004.511.013.765v38.38c0 9.981-8.243 18.205-18.173 18.205H197.149c-1.328 0-2.141-.36-2.728-.777-.686-.486-1.363-1.285-1.806-2.354s-.529-2.115-.384-2.956c.124-.722.455-1.588 1.441-2.575zm173.34-123.001v3.525c.009.399.013.8.013 1.202v108.731c0 33.512-27.312 60.872-60.839 60.872L192 260.151v501.005c0 14.327 11.799 26.112 26.074 26.112h436.147c14.276 0 26.074-11.785 26.074-26.112V111.931c0-14.33-11.797-26.113-26.074-26.113H367.013z", + "M777.485 128.521c-11.785 0-21.333 9.551-21.333 21.333s9.549 21.333 21.333 21.333h28.442c14.276 0 26.074 11.783 26.074 26.113v715.254c0 14.332-11.797 26.112-26.074 26.112H369.78c-14.275 0-26.074-11.785-26.074-26.112v-28.075c0-11.78-9.551-21.333-21.333-21.333s-21.333 9.553-21.333 21.333v28.075c0 37.862 30.868 68.779 68.741 68.779h436.147c37.875 0 68.74-30.916 68.74-68.779V197.3c0-37.861-30.865-68.78-68.74-68.78h-28.442z", + ], + ); diff --git a/src/haz3lweb/app/explainthis/ExplainThis.re b/src/haz3lweb/app/explainthis/ExplainThis.re index 3388bbb3ba..6627136aa2 100644 --- a/src/haz3lweb/app/explainthis/ExplainThis.re +++ b/src/haz3lweb/app/explainthis/ExplainThis.re @@ -2361,11 +2361,11 @@ let section = (~section_clss: string, ~title: string, contents: list(Node.t)) => let get_color_map = (~globals: Globals.t, ~explainThisModel: ExplainThisModel.t, info) => switch (globals.settings.explainThis.highlight) { - | All when globals.settings.explainThis.show => + | All when globals.settings.sidebar.show => let (_, (_, (color_map, _)), _) = get_doc(~globals, ~docs=explainThisModel, info, Colorings); Some(color_map); - | One(id) when globals.settings.explainThis.show => + | One(id) when globals.settings.sidebar.show => let (_, (_, (color_map, _)), _) = get_doc(~globals, ~docs=explainThisModel, info, Colorings); Some(Id.Map.filter((id', _) => id == id', color_map)); @@ -2404,15 +2404,6 @@ let view = Set(ExplainThis(SetHighlight(Toggle))), ) ), - div( - ~attrs=[ - clss(["close"]), - Attr.on_click(_ => - globals.inject_global(Set(ExplainThis(ToggleShow))) - ), - ], - [Icons.thin_x], - ), ], ), ] diff --git a/src/haz3lweb/app/explainthis/ExplainThisModel.re b/src/haz3lweb/app/explainthis/ExplainThisModel.re index e5496b7a37..ec819dff4f 100644 --- a/src/haz3lweb/app/explainthis/ExplainThisModel.re +++ b/src/haz3lweb/app/explainthis/ExplainThisModel.re @@ -44,7 +44,6 @@ module Settings = { [@deriving (show({with_path: false}), sexp, yojson)] type t = { - show: bool, show_feedback: bool, highlight, }; @@ -57,11 +56,10 @@ module Settings = { [@deriving (show({with_path: false}), sexp, yojson)] type action = - | ToggleShow | ToggleShowFeedback | SetHighlight(highlight_action); - let init = {show: true, show_feedback: false, highlight: NoHighlight}; + let init = {show_feedback: false, highlight: NoHighlight}; }; let init: t = {specificity_open: false, forms: [], groups: []}; diff --git a/src/haz3lweb/app/input/Shortcut.re b/src/haz3lweb/app/input/Shortcut.re index 5f664d7361..43972c87ef 100644 --- a/src/haz3lweb/app/input/Shortcut.re +++ b/src/haz3lweb/app/input/Shortcut.re @@ -176,8 +176,8 @@ let shortcuts = (sys: Util.Key.sys): list(t) => mk_shortcut( ~section="Settings", ~mdIcon="tune", - "Toggle Show Docs Sidebar", - Globals(Set(ExplainThis(ToggleShow))), + "Toggle Show Sidebar", + Globals(Set(Sidebar(ToggleShow))), ), mk_shortcut( ~section="Settings", diff --git a/src/haz3lweb/app/inspector/CursorInspector.re b/src/haz3lweb/app/inspector/CursorInspector.re index 2b5a7e340e..a0f3cb85d7 100644 --- a/src/haz3lweb/app/inspector/CursorInspector.re +++ b/src/haz3lweb/app/inspector/CursorInspector.re @@ -14,31 +14,53 @@ let code_box_container = x => let code_err = (code: string): Node.t => div(~attrs=[clss(["code"])], [text(code)]); -let explain_this_toggle = (~globals: Globals.t): Node.t => { - let tooltip = "Toggle language documentation"; - let toggle_explain_this = _ => - Virtual_dom.Vdom.Effect.Many([ - globals.inject_global(Set(ExplainThis(ToggleShow))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[clss(["explain-this-button"])], - [ - Widgets.toggle( - ~tooltip, - "?", - globals.settings.explainThis.show, - toggle_explain_this, - ), - ], - ); -}; +/* + let explain_this_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Switch Language ocumentation"; + let toggle_explain_this = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(ExplainThis(ToggleShow))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["explain-this-button"])], + [ + Widgets.toggle( + ~tooltip, + "?", + globals.settings.explainThis.show, + toggle_explain_this, + ), + ], + ); + }; + + let assistant_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Switch LLM-assistant coder"; + let toggle_assistant = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(ToggleShow))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["assisstant-button"])], + [ + Widgets.toggle( + ~tooltip, + "?", + globals.settings.assistant.show, + toggle_assistant, + ), + ], + ); + }; + */ -let assistant_toggle = (~globals: Globals.t): Node.t => { - let tooltip = "Toggle LLM-assistant coder"; - let toggle_assistant = _ => +let sidebar_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Toggle Sidebar"; + let toggle_sidebar = _ => Virtual_dom.Vdom.Effect.Many([ - globals.inject_global(Set(Assistant(ToggleShow))), + globals.inject_global(Set(Sidebar(ToggleShow))), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( @@ -47,8 +69,8 @@ let assistant_toggle = (~globals: Globals.t): Node.t => { Widgets.toggle( ~tooltip, "?", - globals.settings.assistant.show, - toggle_assistant, + globals.settings.sidebar.show, + toggle_sidebar, ), ], ); @@ -79,9 +101,8 @@ let term_view = (~globals: Globals.t, ci) => { ], [ ctx_toggle(~globals), - div(~attrs=[clss(["term-tag"])], [text(sort)]), - explain_this_toggle(~globals), - assistant_toggle(~globals), + div(~attrs=[clss(["term-tag"])], [text("Sidebar")]), + sidebar_toggle(~globals), // Russ Todo: Remove toggle, add VSCode-like sliding sidebar cls_view(ci), ], ); diff --git a/src/haz3lweb/app/sidebar/Sidebar.re b/src/haz3lweb/app/sidebar/Sidebar.re new file mode 100644 index 0000000000..f2f9f5db35 --- /dev/null +++ b/src/haz3lweb/app/sidebar/Sidebar.re @@ -0,0 +1,55 @@ +open Virtual_dom.Vdom; +open Node; +open Util.Web; +open Util; +open Haz3lcore; + +let tab = (~tooltip="", icon, action) => + div( + ~attrs=[clss(["tab"]), Attr.on_mousedown(action), Attr.title(tooltip)], + [icon], + ); + +let explain_this_tab = (~globals: Globals.t): Node.t => { + let tooltip = "Switch to Language Documentation"; + let switch_explain_this = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global( + Set(Sidebar(SwitchWindow(LanguageDocumentation))), + ), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["explain-this-button"])], + [tab(Icons.explain_this, ~tooltip, switch_explain_this)], + ); +}; + +let assistant_tab = (~globals: Globals.t): Node.t => { + let tooltip = "Switch to Helpful Assistant"; + let switch_assistant = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Sidebar(SwitchWindow(HelpfulAssistant)))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["assistant-button"])], + [tab(Icons.assistant, ~tooltip, switch_assistant)], + ); +}; + +let persistent_view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { + div( + ~attrs=[Attr.id("persistent")], + [ + div( + ~attrs=[clss(["tabs"])], + [explain_this_tab(~globals), assistant_tab(~globals)], + ), + ], + ); +}; + +let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { + div(~attrs=[Attr.id("persistent-sidebar")], []); +}; diff --git a/src/haz3lweb/app/sidebar/SidebarModel.re b/src/haz3lweb/app/sidebar/SidebarModel.re new file mode 100644 index 0000000000..aa3b9b2b70 --- /dev/null +++ b/src/haz3lweb/app/sidebar/SidebarModel.re @@ -0,0 +1,21 @@ +module Sexp = Sexplib.Sexp; +open Haz3lcore; +open Util; + +module Settings = { + [@deriving (show({with_path: false}), sexp, yojson)] + type window = + | LanguageDocumentation + | HelpfulAssistant; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + show: bool, + window, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type action = + | ToggleShow + | SwitchWindow(window); +}; diff --git a/src/haz3lweb/view/NutMenu.re b/src/haz3lweb/view/NutMenu.re index 2155d0f26f..ddb0c92071 100644 --- a/src/haz3lweb/view/NutMenu.re +++ b/src/haz3lweb/view/NutMenu.re @@ -46,12 +46,7 @@ let semantics_group = (~globals) => { ("τ", "Types", globals.settings.core.statics, Statics), ("⇲", "Completion", globals.settings.core.assist, Assist), ("𝛿", "Evaluation", globals.settings.core.dynamics, Dynamics), - ( - "?", - "Docs", - globals.settings.explainThis.show, - ExplainThis(ToggleShow), - ), + ("?", "Docs", globals.settings.sidebar.show, Sidebar(ToggleShow)) // Russ todo: Idk what this does, need to check back // ( // "👍", // "Feedback", diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 01bca28432..2988f27029 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -476,32 +476,28 @@ module View = { ~inject=a => inject(Editors(a)), cursor, ); - let explainThisSidebar = - globals.settings.explainThis.show && globals.settings.core.statics - ? { - ExplainThis.view( - ~globals, - ~inject=action => inject(ExplainThis(action)), - ~explainThisModel, - cursor.info, - ); - } - : { - div([]); - }; - let assistantSidebar = - globals.settings.assistant.show && globals.settings.core.statics - ? { - Assistant.view(~globals, ~inject=_ => Ui_effect.Ignore); - } - : { - div([]); - }; - let sidebars = + let sidebar = { + let sub = + globals.settings.sidebar.show + ? switch (globals.settings.sidebar.window) { + | LanguageDocumentation => + ExplainThis.view( + ~globals, + ~inject=action => inject(ExplainThis(action)), + ~explainThisModel, + cursor.info, + ) + | HelpfulAssistant => + Assistant.view(~globals, ~inject=_ => Ui_effect.Ignore) + } + : { + div([]); + }; div( ~attrs=[Attr.id("sidebars")], - [explainThisSidebar, assistantSidebar], + [sub, Sidebar.persistent_view(~globals, ~inject)], ); + }; let editors_view = Editors.View.view( ~globals, @@ -521,7 +517,7 @@ module View = { ], editors_view, ), - sidebars, + sidebar, bottom_bar, ContextInspector.view(~globals, cursor.info), ]; diff --git a/src/haz3lweb/www/style.css b/src/haz3lweb/www/style.css index 39fe1408d5..c4fd5bab25 100644 --- a/src/haz3lweb/www/style.css +++ b/src/haz3lweb/www/style.css @@ -10,6 +10,7 @@ @import "style/projectors-panel.css"; @import "style/explainthis.css"; @import "style/assistant.css"; +@import "style/sidebar.css"; @import "style/exercise-mode.css"; @import "style/cell.css"; @import "style/dynamics.css"; @@ -103,8 +104,7 @@ select:hover { overflow-y: scroll; overflow-x: hidden; width: max-content; - background-color: var(--ui-bkg); - scrollbar-color: var(--main-scroll-thumb) var(--NONE); + background-color: var(--T2); } #sidebars { diff --git a/src/haz3lweb/www/style/sidebar.css b/src/haz3lweb/www/style/sidebar.css new file mode 100644 index 0000000000..7ae4c179ad --- /dev/null +++ b/src/haz3lweb/www/style/sidebar.css @@ -0,0 +1,57 @@ +/* Persistent Sidebar +#sidebar { + display: flex; + flex: 1; + width: 20em; + padding: 1em; + flex-direction: column; + overflow-y: auto; + gap: 1em; + margin-right: 1em; +} */ + +#sidebar-button { + text-transform: none; +} + +/* Persistent Sidebar */ + +#persistent { + grid-row: 2 / span 1; + grid-column: 2 / span 1; + border-left: 0.6px solid var(--BR2); + overflow-y: scroll; + overflow-x: hidden; + display: flex; + flex: 1; + width: 5em; + flex-direction: column; + gap: 1em; + background-color: var(--ui-bkg); + scrollbar-color: var(--main-scroll-thumb) var(--NONE); +} + +#persistent .tabs { + text-transform: none; +} + +#persistent .tab { + display: flex; + justify-content: center; + align-items: center; + width: 5em; + height: 5em; + cursor: pointer; + transition: background-color 0.2s ease; +} + +#persistent .tab:hover { + background-color: var(--T2); /* Darken tab background on hover */ +} + +/* Optional: Add focus or active state to enhance interaction */ +#persistent .tab:focus, +#persistent .tab:active { + background-color: var(--T2); /* Darken a little more when tab is active */ +} + From 30aa4adc46193cd28d6cccd7f262c47647a2aa8f Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 18 Jan 2025 17:58:02 -0500 Subject: [PATCH 04/50] =?UTF-8?q?=E2=97=80=EF=B8=8F=20=20=E2=96=B6?= =?UTF-8?q?=EF=B8=8F=20=20added=20expand/collapse=20tab=20for=20sidebar=20?= =?UTF-8?q?(removed=20toggle).=20I=20think=20in=20future=20it=20would=20be?= =?UTF-8?q?=20nice=20to=20be=20able=20to=20drag=20the=20sidebar=20closed/o?= =?UTF-8?q?pened?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/app/common/Icons.re | 16 ++++++++++++ src/haz3lweb/app/inspector/CursorInspector.re | 25 +++---------------- src/haz3lweb/app/sidebar/Sidebar.re | 21 +++++++++++++++- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/haz3lweb/app/common/Icons.re b/src/haz3lweb/app/common/Icons.re index 741ddbd66c..edfbcf9f98 100644 --- a/src/haz3lweb/app/common/Icons.re +++ b/src/haz3lweb/app/common/Icons.re @@ -261,3 +261,19 @@ let explain_this = "M777.485 128.521c-11.785 0-21.333 9.551-21.333 21.333s9.549 21.333 21.333 21.333h28.442c14.276 0 26.074 11.783 26.074 26.113v715.254c0 14.332-11.797 26.112-26.074 26.112H369.78c-14.275 0-26.074-11.785-26.074-26.112v-28.075c0-11.78-9.551-21.333-21.333-21.333s-21.333 9.553-21.333 21.333v28.075c0 37.862 30.868 68.779 68.741 68.779h436.147c37.875 0 68.74-30.916 68.74-68.779V197.3c0-37.861-30.865-68.78-68.74-68.78h-28.442z", ], ); + +let expand = + simple_icon( + ~view="0 0 24 24", + [ + "M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z", + ], + ); + +let collapse = + simple_icon( + ~view="0 0 24 24", + [ + "M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z", + ], + ); diff --git a/src/haz3lweb/app/inspector/CursorInspector.re b/src/haz3lweb/app/inspector/CursorInspector.re index a0f3cb85d7..8c0cdc8e63 100644 --- a/src/haz3lweb/app/inspector/CursorInspector.re +++ b/src/haz3lweb/app/inspector/CursorInspector.re @@ -56,26 +56,6 @@ let code_err = (code: string): Node.t => }; */ -let sidebar_toggle = (~globals: Globals.t): Node.t => { - let tooltip = "Toggle Sidebar"; - let toggle_sidebar = _ => - Virtual_dom.Vdom.Effect.Many([ - globals.inject_global(Set(Sidebar(ToggleShow))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[clss(["assisstant-button"])], - [ - Widgets.toggle( - ~tooltip, - "?", - globals.settings.sidebar.show, - toggle_sidebar, - ), - ], - ); -}; - let cls_view = (ci: Info.t): Node.t => div( ~attrs=[clss(["syntax-class"])], @@ -101,8 +81,9 @@ let term_view = (~globals: Globals.t, ci) => { ], [ ctx_toggle(~globals), - div(~attrs=[clss(["term-tag"])], [text("Sidebar")]), - sidebar_toggle(~globals), // Russ Todo: Remove toggle, add VSCode-like sliding sidebar + // russ todo: this bottom-bar needs to be cleaned up now with + // toggle replaced in favor of persisten sidebar + div(~attrs=[clss(["term-tag"])], [text(sort)]), cls_view(ci), ], ); diff --git a/src/haz3lweb/app/sidebar/Sidebar.re b/src/haz3lweb/app/sidebar/Sidebar.re index f2f9f5db35..006ef54726 100644 --- a/src/haz3lweb/app/sidebar/Sidebar.re +++ b/src/haz3lweb/app/sidebar/Sidebar.re @@ -38,13 +38,32 @@ let assistant_tab = (~globals: Globals.t): Node.t => { ); }; +let collapse_tab = (~globals: Globals.t): Node.t => { + let tooltip = + globals.settings.sidebar.show ? "Collapse Sidebar" : "Expand Sidebar"; + let icon = globals.settings.sidebar.show ? Icons.collapse : Icons.expand; + let switch_assistant = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Sidebar(ToggleShow))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["collapse-button"])], + [tab(icon, ~tooltip, switch_assistant)], + ); +}; + let persistent_view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { div( ~attrs=[Attr.id("persistent")], [ div( ~attrs=[clss(["tabs"])], - [explain_this_tab(~globals), assistant_tab(~globals)], + [ + explain_this_tab(~globals), + assistant_tab(~globals), + collapse_tab(~globals), + ], ), ], ); From 3bebab344373ee72a99503283b1579a519e96abe Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 19 Jan 2025 09:59:19 -0500 Subject: [PATCH 05/50] added indicator icon to indicate which tab user is on in sidebar --- src/haz3lweb/app/assistant/Assistant.re | 2 +- src/haz3lweb/app/sidebar/Sidebar.re | 26 ++++++++++++++++++++----- src/haz3lweb/www/style/explainthis.css | 1 - src/haz3lweb/www/style/sidebar.css | 16 +++++++++++++-- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/haz3lweb/app/assistant/Assistant.re b/src/haz3lweb/app/assistant/Assistant.re index fc333e7f4b..b8aa2f02b2 100644 --- a/src/haz3lweb/app/assistant/Assistant.re +++ b/src/haz3lweb/app/assistant/Assistant.re @@ -13,7 +13,7 @@ let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { [ div( ~attrs=[clss(["assistant-title"])], - [text("Assistant Chat")], + [text("Agentic Assistant Chat")], ), ], ), diff --git a/src/haz3lweb/app/sidebar/Sidebar.re b/src/haz3lweb/app/sidebar/Sidebar.re index 006ef54726..a16dee6e50 100644 --- a/src/haz3lweb/app/sidebar/Sidebar.re +++ b/src/haz3lweb/app/sidebar/Sidebar.re @@ -4,11 +4,13 @@ open Util.Web; open Util; open Haz3lcore; -let tab = (~tooltip="", icon, action) => +let tab = (~tooltip="", icon, action, isActive) => { + let classes = ["tab"] @ (isActive ? ["active"] : []); div( - ~attrs=[clss(["tab"]), Attr.on_mousedown(action), Attr.title(tooltip)], + ~attrs=[clss(classes), Attr.on_mousedown(action), Attr.title(tooltip)], [icon], ); +}; let explain_this_tab = (~globals: Globals.t): Node.t => { let tooltip = "Switch to Language Documentation"; @@ -21,7 +23,14 @@ let explain_this_tab = (~globals: Globals.t): Node.t => { ]); div( ~attrs=[clss(["explain-this-button"])], - [tab(Icons.explain_this, ~tooltip, switch_explain_this)], + [ + tab( + Icons.explain_this, + ~tooltip, + switch_explain_this, + globals.settings.sidebar.window == LanguageDocumentation, + ), + ], ); }; @@ -34,7 +43,14 @@ let assistant_tab = (~globals: Globals.t): Node.t => { ]); div( ~attrs=[clss(["assistant-button"])], - [tab(Icons.assistant, ~tooltip, switch_assistant)], + [ + tab( + Icons.assistant, + ~tooltip, + switch_assistant, + globals.settings.sidebar.window == HelpfulAssistant, + ), + ], ); }; @@ -49,7 +65,7 @@ let collapse_tab = (~globals: Globals.t): Node.t => { ]); div( ~attrs=[clss(["collapse-button"])], - [tab(icon, ~tooltip, switch_assistant)], + [tab(icon, ~tooltip, switch_assistant, false)], ); }; diff --git a/src/haz3lweb/www/style/explainthis.css b/src/haz3lweb/www/style/explainthis.css index 7ac7e1d48d..78a117c3f1 100644 --- a/src/haz3lweb/www/style/explainthis.css +++ b/src/haz3lweb/www/style/explainthis.css @@ -6,7 +6,6 @@ flex-direction: column; overflow-y: auto; gap: 1em; - margin-right: 1em; } #explain-this #examples { diff --git a/src/haz3lweb/www/style/sidebar.css b/src/haz3lweb/www/style/sidebar.css index 7ae4c179ad..eb2c345a04 100644 --- a/src/haz3lweb/www/style/sidebar.css +++ b/src/haz3lweb/www/style/sidebar.css @@ -32,19 +32,31 @@ } #persistent .tabs { - text-transform: none; + display: flex; + flex-direction: column; + flex-grow: 1; } #persistent .tab { + box-sizing: border-box; display: flex; justify-content: center; align-items: center; width: 5em; height: 5em; - cursor: pointer; + cursor: pointer; transition: background-color 0.2s ease; } +.collapse-button { + margin-top: auto; +} + +#persistent .tab.active { + border-right: .2em solid var(--BR3); + padding-left: .2em; +} + #persistent .tab:hover { background-color: var(--T2); /* Darken tab background on hover */ } From f5af748430ba0d8a6436b54f5543a657b8b02322 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 19 Jan 2025 12:56:09 -0500 Subject: [PATCH 06/50] toggle manual llm/lsp added; begin/end chat button added --- src/haz3lweb/Settings.re | 26 ++--- src/haz3lweb/app/assistant/Assistant.re | 22 ---- src/haz3lweb/app/sidebar/Sidebar.re | 4 - .../app/sidebar/assistant/Assistant.re | 107 ++++++++++++++++++ .../{ => sidebar}/assistant/AssistantModel.re | 12 +- .../{ => sidebar}/explainthis/ColorSteps.re | 0 .../app/{ => sidebar}/explainthis/Example.re | 0 .../{ => sidebar}/explainthis/ExplainThis.re | 0 .../explainthis/ExplainThisForm.re | 0 .../explainthis/ExplainThisModel.re | 0 .../{ => sidebar}/explainthis/data/AppExp.re | 0 .../{ => sidebar}/explainthis/data/AppPat.re | 0 .../explainthis/data/ArrowTyp.re | 0 .../{ => sidebar}/explainthis/data/CaseExp.re | 0 .../explainthis/data/FilterExp.re | 0 .../{ => sidebar}/explainthis/data/FixFExp.re | 0 .../explainthis/data/ForallTyp.re | 0 .../explainthis/data/FunctionExp.re | 0 .../{ => sidebar}/explainthis/data/HoleExp.re | 0 .../{ => sidebar}/explainthis/data/HolePat.re | 0 .../explainthis/data/HoleTPat.re | 0 .../explainthis/data/HoleTemplate.re | 0 .../{ => sidebar}/explainthis/data/HoleTyp.re | 0 .../{ => sidebar}/explainthis/data/IfExp.re | 0 .../{ => sidebar}/explainthis/data/LetExp.re | 0 .../{ => sidebar}/explainthis/data/ListExp.re | 0 .../{ => sidebar}/explainthis/data/ListPat.re | 0 .../{ => sidebar}/explainthis/data/ListTyp.re | 0 .../{ => sidebar}/explainthis/data/OpExp.re | 0 .../explainthis/data/PipelineExp.re | 0 .../{ => sidebar}/explainthis/data/RecTyp.re | 0 .../{ => sidebar}/explainthis/data/SeqExp.re | 0 .../{ => sidebar}/explainthis/data/SumTyp.re | 0 .../explainthis/data/TerminalExp.re | 0 .../explainthis/data/TerminalPat.re | 0 .../explainthis/data/TerminalTyp.re | 0 .../{ => sidebar}/explainthis/data/TestExp.re | 0 .../explainthis/data/TupleExp.re | 0 .../explainthis/data/TuplePat.re | 0 .../explainthis/data/TupleTyp.re | 0 .../explainthis/data/TyAliasExp.re | 0 .../explainthis/data/TypAnnPat.re | 0 .../explainthis/data/TypAppExp.re | 0 .../explainthis/data/TypFunctionExp.re | 0 .../explainthis/data/UndefinedExp.re | 0 .../{ => sidebar}/explainthis/data/VarTPat.re | 0 src/haz3lweb/www/style/assistant.css | 45 +++++++- 47 files changed, 169 insertions(+), 47 deletions(-) delete mode 100644 src/haz3lweb/app/assistant/Assistant.re create mode 100644 src/haz3lweb/app/sidebar/assistant/Assistant.re rename src/haz3lweb/app/{ => sidebar}/assistant/AssistantModel.re (75%) rename src/haz3lweb/app/{ => sidebar}/explainthis/ColorSteps.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/Example.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/ExplainThis.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/ExplainThisForm.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/ExplainThisModel.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/AppExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/AppPat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/ArrowTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/CaseExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/FilterExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/FixFExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/ForallTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/FunctionExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/HoleExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/HolePat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/HoleTPat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/HoleTemplate.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/HoleTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/IfExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/LetExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/ListExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/ListPat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/ListTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/OpExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/PipelineExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/RecTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/SeqExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/SumTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TerminalExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TerminalPat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TerminalTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TestExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TupleExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TuplePat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TupleTyp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TyAliasExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TypAnnPat.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TypAppExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/TypFunctionExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/UndefinedExp.re (100%) rename src/haz3lweb/app/{ => sidebar}/explainthis/data/VarTPat.re (100%) diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index d49f417e13..67fb485c6a 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -44,8 +44,9 @@ module Model = { highlight: NoHighlight, }, assistant: { - llm: Human, - lsp: Human, + llm: false, + lsp: false, + ongoing_chat: false, }, sidebar: { window: LanguageDocumentation, @@ -195,8 +196,8 @@ module Update = { | Sidebar(SwitchWindow(windowToSwitchTo)) => { ...settings, sidebar: { - ...settings.sidebar, window: windowToSwitchTo, + show: true, }, } | ExplainThis(ToggleShowFeedback) => { @@ -222,22 +223,21 @@ module Update = { ...settings, assistant: { ...settings.assistant, - llm: - switch (settings.assistant.llm) { - | Agent => Human - | Human => Agent - }, + llm: !settings.assistant.llm, }, } | Assistant(ToggleLSP) => { ...settings, assistant: { ...settings.assistant, - lsp: - switch (settings.assistant.lsp) { - | LanguageServer => Human - | Human => LanguageServer - }, + lsp: !settings.assistant.lsp, + }, + } + | Assistant(UpdateChatStatus) => { + ...settings, + assistant: { + ...settings.assistant, + ongoing_chat: !settings.assistant.ongoing_chat, }, } | Benchmark => {...settings, benchmark: !settings.benchmark} diff --git a/src/haz3lweb/app/assistant/Assistant.re b/src/haz3lweb/app/assistant/Assistant.re deleted file mode 100644 index b8aa2f02b2..0000000000 --- a/src/haz3lweb/app/assistant/Assistant.re +++ /dev/null @@ -1,22 +0,0 @@ -open Virtual_dom.Vdom; -open Node; -open Util.Web; -open Util; -open Haz3lcore; - -let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { - div( - ~attrs=[Attr.id("side-bar")], - [ - div( - ~attrs=[Attr.id("assistant")], - [ - div( - ~attrs=[clss(["assistant-title"])], - [text("Agentic Assistant Chat")], - ), - ], - ), - ], - ); -}; diff --git a/src/haz3lweb/app/sidebar/Sidebar.re b/src/haz3lweb/app/sidebar/Sidebar.re index a16dee6e50..1b9f2d4ac7 100644 --- a/src/haz3lweb/app/sidebar/Sidebar.re +++ b/src/haz3lweb/app/sidebar/Sidebar.re @@ -84,7 +84,3 @@ let persistent_view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { ], ); }; - -let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { - div(~attrs=[Attr.id("persistent-sidebar")], []); -}; diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re new file mode 100644 index 0000000000..2e185676c2 --- /dev/null +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -0,0 +1,107 @@ +open Virtual_dom.Vdom; +open Node; +open Util.Web; +open Util; +open Haz3lcore; + +let llm_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Toggle Manual LLM"; + let toggle_llm = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(ToggleLLM))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["llm-button"])], + [ + text("Manual LLM: "), + Widgets.toggle( + ~tooltip, + "🕵️‍♀️", + globals.settings.assistant.llm, + toggle_llm, + ), + ], + ); +}; + +let lsp_toggle = (~globals: Globals.t): Node.t => { + let tooltip = "Toggle Manual LSP"; + let toggle_lsp = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(ToggleLSP))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["lsp-button"])], + [ + text("Manual LSP: "), + Widgets.toggle( + ~tooltip, + "🧑‍🔧", + globals.settings.assistant.lsp, + toggle_lsp, + ), + ], + ); +}; + +let begin_chat_button = (~globals: Globals.t): Node.t => { + let tooltip = "Begin Chat"; + let begin_chat = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(UpdateChatStatus))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(begin_chat)], + [Widgets.button_named(~tooltip, None, begin_chat)], + ); +}; + +let end_chat_button = (~globals: Globals.t): Node.t => { + let tooltip = "End Chat"; + let end_chat = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(UpdateChatStatus))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(end_chat)], + [Widgets.button_named(~tooltip, None, end_chat)], + ); +}; + +let settings_box = (~globals: Globals.t): Node.t => { + div( + ~attrs=[clss(["settings-box"])], + [ + llm_toggle(~globals), + lsp_toggle(~globals), + begin_chat_button(~globals), + ], + ); +}; + +let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { + div( + ~attrs=[Attr.id("side-bar")], + [ + div( + ~attrs=[Attr.id("assistant")], + [ + div( + ~attrs=[clss(["header"])], + [ + text("Agentic Assistant Chat"), + globals.settings.assistant.ongoing_chat + ? end_chat_button(~globals) : None, + ], + ), + globals.settings.assistant.ongoing_chat + ? None : settings_box(~globals), + ], + ), + ], + ); +}; diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re similarity index 75% rename from src/haz3lweb/app/assistant/AssistantModel.re rename to src/haz3lweb/app/sidebar/assistant/AssistantModel.re index 72f0f121d9..37994c7f0d 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -4,23 +4,25 @@ open Util; module Settings = { [@deriving (show({with_path: false}), sexp, yojson)] - type llm_input = + type manual_llm = | Agent | Human; [@deriving (show({with_path: false}), sexp, yojson)] - type lsp_input = + type manual_lsp = | LanguageServer | Human; [@deriving (show({with_path: false}), sexp, yojson)] type t = { - llm: llm_input, - lsp: lsp_input, + llm: bool, + lsp: bool, + ongoing_chat: bool, }; [@deriving (show({with_path: false}), sexp, yojson)] type action = | ToggleLLM - | ToggleLSP; + | ToggleLSP + | UpdateChatStatus; }; diff --git a/src/haz3lweb/app/explainthis/ColorSteps.re b/src/haz3lweb/app/sidebar/explainthis/ColorSteps.re similarity index 100% rename from src/haz3lweb/app/explainthis/ColorSteps.re rename to src/haz3lweb/app/sidebar/explainthis/ColorSteps.re diff --git a/src/haz3lweb/app/explainthis/Example.re b/src/haz3lweb/app/sidebar/explainthis/Example.re similarity index 100% rename from src/haz3lweb/app/explainthis/Example.re rename to src/haz3lweb/app/sidebar/explainthis/Example.re diff --git a/src/haz3lweb/app/explainthis/ExplainThis.re b/src/haz3lweb/app/sidebar/explainthis/ExplainThis.re similarity index 100% rename from src/haz3lweb/app/explainthis/ExplainThis.re rename to src/haz3lweb/app/sidebar/explainthis/ExplainThis.re diff --git a/src/haz3lweb/app/explainthis/ExplainThisForm.re b/src/haz3lweb/app/sidebar/explainthis/ExplainThisForm.re similarity index 100% rename from src/haz3lweb/app/explainthis/ExplainThisForm.re rename to src/haz3lweb/app/sidebar/explainthis/ExplainThisForm.re diff --git a/src/haz3lweb/app/explainthis/ExplainThisModel.re b/src/haz3lweb/app/sidebar/explainthis/ExplainThisModel.re similarity index 100% rename from src/haz3lweb/app/explainthis/ExplainThisModel.re rename to src/haz3lweb/app/sidebar/explainthis/ExplainThisModel.re diff --git a/src/haz3lweb/app/explainthis/data/AppExp.re b/src/haz3lweb/app/sidebar/explainthis/data/AppExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/AppExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/AppExp.re diff --git a/src/haz3lweb/app/explainthis/data/AppPat.re b/src/haz3lweb/app/sidebar/explainthis/data/AppPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/AppPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/AppPat.re diff --git a/src/haz3lweb/app/explainthis/data/ArrowTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/ArrowTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/ArrowTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/ArrowTyp.re diff --git a/src/haz3lweb/app/explainthis/data/CaseExp.re b/src/haz3lweb/app/sidebar/explainthis/data/CaseExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/CaseExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/CaseExp.re diff --git a/src/haz3lweb/app/explainthis/data/FilterExp.re b/src/haz3lweb/app/sidebar/explainthis/data/FilterExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/FilterExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/FilterExp.re diff --git a/src/haz3lweb/app/explainthis/data/FixFExp.re b/src/haz3lweb/app/sidebar/explainthis/data/FixFExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/FixFExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/FixFExp.re diff --git a/src/haz3lweb/app/explainthis/data/ForallTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/ForallTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/ForallTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/ForallTyp.re diff --git a/src/haz3lweb/app/explainthis/data/FunctionExp.re b/src/haz3lweb/app/sidebar/explainthis/data/FunctionExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/FunctionExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/FunctionExp.re diff --git a/src/haz3lweb/app/explainthis/data/HoleExp.re b/src/haz3lweb/app/sidebar/explainthis/data/HoleExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/HoleExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/HoleExp.re diff --git a/src/haz3lweb/app/explainthis/data/HolePat.re b/src/haz3lweb/app/sidebar/explainthis/data/HolePat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/HolePat.re rename to src/haz3lweb/app/sidebar/explainthis/data/HolePat.re diff --git a/src/haz3lweb/app/explainthis/data/HoleTPat.re b/src/haz3lweb/app/sidebar/explainthis/data/HoleTPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/HoleTPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/HoleTPat.re diff --git a/src/haz3lweb/app/explainthis/data/HoleTemplate.re b/src/haz3lweb/app/sidebar/explainthis/data/HoleTemplate.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/HoleTemplate.re rename to src/haz3lweb/app/sidebar/explainthis/data/HoleTemplate.re diff --git a/src/haz3lweb/app/explainthis/data/HoleTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/HoleTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/HoleTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/HoleTyp.re diff --git a/src/haz3lweb/app/explainthis/data/IfExp.re b/src/haz3lweb/app/sidebar/explainthis/data/IfExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/IfExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/IfExp.re diff --git a/src/haz3lweb/app/explainthis/data/LetExp.re b/src/haz3lweb/app/sidebar/explainthis/data/LetExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/LetExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/LetExp.re diff --git a/src/haz3lweb/app/explainthis/data/ListExp.re b/src/haz3lweb/app/sidebar/explainthis/data/ListExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/ListExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/ListExp.re diff --git a/src/haz3lweb/app/explainthis/data/ListPat.re b/src/haz3lweb/app/sidebar/explainthis/data/ListPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/ListPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/ListPat.re diff --git a/src/haz3lweb/app/explainthis/data/ListTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/ListTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/ListTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/ListTyp.re diff --git a/src/haz3lweb/app/explainthis/data/OpExp.re b/src/haz3lweb/app/sidebar/explainthis/data/OpExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/OpExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/OpExp.re diff --git a/src/haz3lweb/app/explainthis/data/PipelineExp.re b/src/haz3lweb/app/sidebar/explainthis/data/PipelineExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/PipelineExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/PipelineExp.re diff --git a/src/haz3lweb/app/explainthis/data/RecTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/RecTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/RecTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/RecTyp.re diff --git a/src/haz3lweb/app/explainthis/data/SeqExp.re b/src/haz3lweb/app/sidebar/explainthis/data/SeqExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/SeqExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/SeqExp.re diff --git a/src/haz3lweb/app/explainthis/data/SumTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/SumTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/SumTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/SumTyp.re diff --git a/src/haz3lweb/app/explainthis/data/TerminalExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TerminalExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TerminalExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TerminalExp.re diff --git a/src/haz3lweb/app/explainthis/data/TerminalPat.re b/src/haz3lweb/app/sidebar/explainthis/data/TerminalPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TerminalPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/TerminalPat.re diff --git a/src/haz3lweb/app/explainthis/data/TerminalTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/TerminalTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TerminalTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TerminalTyp.re diff --git a/src/haz3lweb/app/explainthis/data/TestExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TestExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TestExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TestExp.re diff --git a/src/haz3lweb/app/explainthis/data/TupleExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TupleExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TupleExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TupleExp.re diff --git a/src/haz3lweb/app/explainthis/data/TuplePat.re b/src/haz3lweb/app/sidebar/explainthis/data/TuplePat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TuplePat.re rename to src/haz3lweb/app/sidebar/explainthis/data/TuplePat.re diff --git a/src/haz3lweb/app/explainthis/data/TupleTyp.re b/src/haz3lweb/app/sidebar/explainthis/data/TupleTyp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TupleTyp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TupleTyp.re diff --git a/src/haz3lweb/app/explainthis/data/TyAliasExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TyAliasExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TyAliasExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TyAliasExp.re diff --git a/src/haz3lweb/app/explainthis/data/TypAnnPat.re b/src/haz3lweb/app/sidebar/explainthis/data/TypAnnPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TypAnnPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/TypAnnPat.re diff --git a/src/haz3lweb/app/explainthis/data/TypAppExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TypAppExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TypAppExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TypAppExp.re diff --git a/src/haz3lweb/app/explainthis/data/TypFunctionExp.re b/src/haz3lweb/app/sidebar/explainthis/data/TypFunctionExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/TypFunctionExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/TypFunctionExp.re diff --git a/src/haz3lweb/app/explainthis/data/UndefinedExp.re b/src/haz3lweb/app/sidebar/explainthis/data/UndefinedExp.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/UndefinedExp.re rename to src/haz3lweb/app/sidebar/explainthis/data/UndefinedExp.re diff --git a/src/haz3lweb/app/explainthis/data/VarTPat.re b/src/haz3lweb/app/sidebar/explainthis/data/VarTPat.re similarity index 100% rename from src/haz3lweb/app/explainthis/data/VarTPat.re rename to src/haz3lweb/app/sidebar/explainthis/data/VarTPat.re diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index a1ad646a52..2a0daa272e 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -12,9 +12,48 @@ text-transform: none; } -#assistant .assistant-title { +#assistant .header { + display: flex; text-transform: none; + font-size: 1em; font-weight: bold; color: var(--ui-header-text); - font-size: 1.2em; -} \ No newline at end of file + justify-content: space-between; + align-items: center; + margin-bottom: 1em; +} + +#assistant .settings-box { + padding: 1em; + display: flex; + flex-direction: column; + align-items: center; + gap: 2.25em; + margin-top: 1em; +} + +#assistant .chat-button { + background-color: var(--SAND); + color: var(--STONE); + font-size: .75em; + padding: 0.5em 1em; + border-radius: 4px; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + width: auto; + height: 1.25em; + cursor: pointer; + transition: all 0.3s ease; +} + +#assistant .chat-button:hover { + background-color: var(--T1); + box-shadow: 0px 4px 6px var(--SHADOW); +} + +#assistant .chat-button:active { + background-color: var(--T3); +} + \ No newline at end of file From c24fa16e6342a32b19778bce212bea6c0bc0d8d7 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 19 Jan 2025 17:05:29 -0500 Subject: [PATCH 07/50] =?UTF-8?q?=F0=9F=92=AC=20chat=20bar=20and=20send=20?= =?UTF-8?q?button=20created;=20added=20message=20list=20to=20assistant=20m?= =?UTF-8?q?odel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/Store.re | 2 + src/haz3lweb/app/common/Icons.re | 7 +++ .../app/sidebar/assistant/Assistant.re | 54 +++++++++++++++++- .../app/sidebar/assistant/AssistantModel.re | 31 +++++++++++ src/haz3lweb/app/sidebar/assistant/Chat.re | 3 + .../sidebar}/explainthis/ExplainThisUpdate.re | 0 src/haz3lweb/view/Page.re | 34 ++++++++---- src/haz3lweb/www/style/assistant.css | 55 ++++++++++++++++++- 8 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 src/haz3lweb/app/sidebar/assistant/Chat.re rename src/haz3lweb/{ => app/sidebar}/explainthis/ExplainThisUpdate.re (100%) diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index 0665dcbce7..932f56f1e2 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -4,6 +4,7 @@ open Util; type key = | Settings | ExplainThis + | Assistant | Mode | Scratch | Documentation @@ -14,6 +15,7 @@ let key_to_string = fun | Settings => "SETTINGS" | ExplainThis => "ExplainThisModel" + | Assistant => "AssistantModel" | Mode => "MODE" | Scratch => "SAVE_SCRATCH" | Documentation => "SAVE_DOCUMENTATION" diff --git a/src/haz3lweb/app/common/Icons.re b/src/haz3lweb/app/common/Icons.re index edfbcf9f98..3f5154c23f 100644 --- a/src/haz3lweb/app/common/Icons.re +++ b/src/haz3lweb/app/common/Icons.re @@ -277,3 +277,10 @@ let collapse = "M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z", ], ); +let send = + simple_icon( + ~view="0 0 24 24", + [ + "M11.4697 3.46967C11.7626 3.17678 12.2374 3.17678 12.5303 3.46967L18.5303 9.46967C18.8232 9.76256 18.8232 10.2374 18.5303 10.5303C18.2374 10.8232 17.7626 10.8232 17.4697 10.5303L12.75 5.81066L12.75 20C12.75 20.4142 12.4142 20.75 12 20.75C11.5858 20.75 11.25 20.4142 11.25 20L11.25 5.81066L6.53033 10.5303C6.23744 10.8232 5.76256 10.8232 5.46967 10.5303C5.17678 10.2374 5.17678 9.76256 5.46967 9.46967L11.4697 3.46967Z", + ], + ); diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index 2e185676c2..d8716b4590 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -3,6 +3,7 @@ open Node; open Util.Web; open Util; open Haz3lcore; +open Js_of_ocaml; let llm_toggle = (~globals: Globals.t): Node.t => { let tooltip = "Toggle Manual LLM"; @@ -83,7 +84,51 @@ let settings_box = (~globals: Globals.t): Node.t => { ); }; -let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { +let message_input = (~inject, ~globals: Globals.t): Node.t => { + let handle_send = (message: string) => { + JsUtil.log("Message sent: " ++ message); + Virtual_dom.Vdom.Effect.Many([ + inject(AssistantModel.Update.SendMessage(message)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + }; + + let send_message = _ => { + let message = + Js.Opt.case( + Dom_html.document##getElementById(Js.string("message-input")), + () => "", + el => + switch (Js.Unsafe.coerce(el)) { + | input => Js.to_string(input##.value) + }, + ); + handle_send(message); + }; + + div( + ~attrs=[clss(["input-container"])], + [ + input( + ~attrs=[ + Attr.id("message-input"), + Attr.placeholder("Type a message..."), + Attr.type_("text"), + clss(["message-input"]), + ], + (), + ), + div( + ~attrs=[clss(["send-button"])], + [ + Widgets.button(~tooltip="Submit Message", Icons.send, send_message), + ], + ), + ], + ); +}; + +let view = (~globals: Globals.t, ~inject, ~assistantModel) => { div( ~attrs=[Attr.id("side-bar")], [ @@ -93,13 +138,18 @@ let view = (~globals: Globals.t, ~inject: 'a => Effect.t(unit)) => { div( ~attrs=[clss(["header"])], [ - text("Agentic Assistant Chat"), + div( + ~attrs=[clss(["title"])], + [text("Agentic Assistant Chat")], + ), globals.settings.assistant.ongoing_chat ? end_chat_button(~globals) : None, ], ), globals.settings.assistant.ongoing_chat ? None : settings_box(~globals), + globals.settings.assistant.ongoing_chat + ? message_input(~inject, ~globals) : None, ], ), ], diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re index 37994c7f0d..db83cce8b9 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -26,3 +26,34 @@ module Settings = { | ToggleLSP | UpdateChatStatus; }; + +module Model = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = {chat: list(string)}; + + [@deriving (show({with_path: false}), sexp, yojson)] + let init: t = {chat: []}; +}; + +module Update = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = + | SendMessage(string); + + let update = + (~settings: Settings.t, action, model: Model.t): Updated.t(Model.t) => { + switch (action) { + | SendMessage(message) => + print_endline(message); + Model.{chat: ["updated", "and testing"]} |> Updated.return_quiet; + }; + }; +}; + +module Store = + Store.F({ + [@deriving (show({with_path: false}), yojson, sexp)] + type t = Model.t; + let default = () => Model.init; + let key = Store.Assistant; + }); diff --git a/src/haz3lweb/app/sidebar/assistant/Chat.re b/src/haz3lweb/app/sidebar/assistant/Chat.re new file mode 100644 index 0000000000..de6d1860d0 --- /dev/null +++ b/src/haz3lweb/app/sidebar/assistant/Chat.re @@ -0,0 +1,3 @@ +open Util; +open Haz3lcore; +open Web; diff --git a/src/haz3lweb/explainthis/ExplainThisUpdate.re b/src/haz3lweb/app/sidebar/explainthis/ExplainThisUpdate.re similarity index 100% rename from src/haz3lweb/explainthis/ExplainThisUpdate.re rename to src/haz3lweb/app/sidebar/explainthis/ExplainThisUpdate.re diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 2988f27029..cc72d6c167 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -16,6 +16,7 @@ module Model = { globals: Globals.Model.t, editors: Editors.Model.t, explain_this: ExplainThisModel.t, + assistant: AssistantModel.Model.t, selection, }; @@ -31,21 +32,14 @@ module Store = { ~instructor_mode=globals.settings.instructor_mode, ); let explain_this = ExplainThisModel.Store.load(); + let assistant = AssistantModel.Store.load(); { editors, globals, explain_this, + assistant, selection: Editors.Selection.default_selection(editors), }; - /* Russ todo: - let assistant = Assist.Store.load(); - { - editors, - globals, - explain_this, - selection: Editors.Selection.default_selection(editors), - }; - */ }; let save = (m: Model.t): unit => { @@ -55,7 +49,7 @@ module Store = { ); Globals.Model.save(m.globals); ExplainThisModel.Store.save(m.explain_this); - /* Russ todo: Assistant.Store.save(); */ + AssistantModel.Store.save(m.assistant); }; }; @@ -72,6 +66,7 @@ module Update = { | Globals(Globals.Update.t) | Editors(Editors.Update.t) | ExplainThis(ExplainThisUpdate.update) + | Assistant(AssistantModel.Update.t) | MakeActive(selection) | Benchmark(benchmark_action) | Start @@ -241,6 +236,11 @@ module Update = { let* explain_this = ExplainThisUpdate.set_update(model.explain_this, action); {...model, explain_this}; + | Assistant(action) => + let settings = globals.settings.assistant; + let* assistant = + AssistantModel.Update.update(~settings, action, model.assistant); + {...model, assistant}; | MakeActive(selection) => {...model, selection} |> Updated.return | Benchmark(Start) => List.iter(a => schedule_action(Editors(a)), Benchmark.actions_1); @@ -462,7 +462,13 @@ module View = { ~get_log_and: (string => unit) => unit, ~inject: Update.t => Ui_effect.t(unit), ~cursor: Cursor.cursor(Editors.Update.t), - {globals, editors, explain_this: explainThisModel, selection} as model: Model.t, + { + globals, + editors, + explain_this: explainThisModel, + assistant: assistantModel, + selection, + } as model: Model.t, ) => { let globals = { ...globals, @@ -488,7 +494,11 @@ module View = { cursor.info, ) | HelpfulAssistant => - Assistant.view(~globals, ~inject=_ => Ui_effect.Ignore) + Assistant.view( + ~globals, + ~inject=action => inject(Assistant(action)), + ~assistantModel, + ) } : { div([]); diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 2a0daa272e..5ace8b2b1e 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -12,12 +12,19 @@ text-transform: none; } -#assistant .header { - display: flex; +#assistant .title { text-transform: none; font-size: 1em; font-weight: bold; + padding-top: .5em; + padding-bottom: .5em; color: var(--ui-header-text); +} + +#assistant .header { + display: flex; + text-transform: none; + font-weight: bold; justify-content: space-between; align-items: center; margin-bottom: 1em; @@ -56,4 +63,46 @@ #assistant .chat-button:active { background-color: var(--T3); } - \ No newline at end of file + +#assistant .input-container { + align-items: center; + padding: 1em 0em; + width: 100%; + display: flex; + gap: 10px; +} + +#assistant .message-input { + flex: 1; + background-color: var(--SAND); + padding: 8px; + outline: none; + border-radius: 3.5px; + border: none; + transition: all 0.3s ease; +} + +#assistant .message-input:focus { + box-shadow: 0px 0px 1px var(--SHADOW), 0 0 0 1px var(--SHADOW); +} + +#assistant .send-button { + background-color: var(--SAND); + color: var(--STONE); + padding: .65em .75em; + border: 0 solid var(--SAND); + border-radius: .5em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +#assistant .send-button:hover { + background-color: var(--T1); + box-shadow: 0px 4px 6px var(--SHADOW); +} + +#assistant .send-button:active { + background-color: var(--T3); +} + From 915d8c131c51a3429ed81e78a74f7f99a165939a Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 19 Jan 2025 21:42:57 -0500 Subject: [PATCH 08/50] implemented switching focus from code editor to text-box when typing in assistant text-box; O_o --- src/haz3lweb/app/editors/Editors.re | 9 +-- .../app/sidebar/assistant/Assistant.re | 15 ++++- src/haz3lweb/view/Page.re | 10 +++- src/haz3lweb/view/ScratchMode.re | 60 ++++++++++++------- src/haz3lweb/www/style.css | 4 +- src/haz3lweb/www/style/assistant.css | 6 +- 6 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/haz3lweb/app/editors/Editors.re b/src/haz3lweb/app/editors/Editors.re index fffd026843..fdd27b85d9 100644 --- a/src/haz3lweb/app/editors/Editors.re +++ b/src/haz3lweb/app/editors/Editors.re @@ -233,8 +233,9 @@ module Selection = { let default_selection = fun - | Model.Scratch(_) => Scratch(MainEditor) - | Model.Documentation(_) => Scratch(MainEditor) + | Model.Scratch(_) => Scratch(ScratchMode.Selection.Cell(MainEditor)) + | Model.Documentation(_) => + Scratch(ScratchMode.Selection.Cell(MainEditor)) | Model.Exercises(_) => Exercises((Exercise.Prelude, MainEditor)); }; @@ -260,7 +261,7 @@ module View = { fun | MakeActive(s) => signal(MakeActive(Scratch(s))), ~globals, - ~selected= + ~selection= switch (selection) { | Some(Scratch(s)) => Some(s) | _ => None @@ -274,7 +275,7 @@ module View = { fun | MakeActive(s) => signal(MakeActive(Scratch(s))), ~globals, - ~selected= + ~selection= switch (selection) { | Some(Scratch(s)) => Some(s) | _ => None diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index d8716b4590..74148484b3 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -5,6 +5,12 @@ open Util; open Haz3lcore; open Js_of_ocaml; +type selection = + | MakeActive(Selection.t); + +type event = + | MakeActive(ScratchMode.Selection.t); + let llm_toggle = (~globals: Globals.t): Node.t => { let tooltip = "Toggle Manual LLM"; let toggle_llm = _ => @@ -84,7 +90,7 @@ let settings_box = (~globals: Globals.t): Node.t => { ); }; -let message_input = (~inject, ~globals: Globals.t): Node.t => { +let message_input = (~signal, ~inject, ~globals: Globals.t): Node.t => { let handle_send = (message: string) => { JsUtil.log("Message sent: " ++ message); Virtual_dom.Vdom.Effect.Many([ @@ -114,6 +120,9 @@ let message_input = (~inject, ~globals: Globals.t): Node.t => { Attr.id("message-input"), Attr.placeholder("Type a message..."), Attr.type_("text"), + Attr.on_focus(_ => + signal(MakeActive(ScratchMode.Selection.TextBox)) + ), clss(["message-input"]), ], (), @@ -128,7 +137,7 @@ let message_input = (~inject, ~globals: Globals.t): Node.t => { ); }; -let view = (~globals: Globals.t, ~inject, ~assistantModel) => { +let view = (~globals: Globals.t, ~signal, ~inject, ~assistantModel) => { div( ~attrs=[Attr.id("side-bar")], [ @@ -149,7 +158,7 @@ let view = (~globals: Globals.t, ~inject, ~assistantModel) => { globals.settings.assistant.ongoing_chat ? None : settings_box(~globals), globals.settings.assistant.ongoing_chat - ? message_input(~inject, ~globals) : None, + ? message_input(~signal, ~inject, ~globals) : None, ], ), ], diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index cc72d6c167..ef30f46919 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -494,11 +494,19 @@ module View = { cursor.info, ) | HelpfulAssistant => + open Editors.View; + let signal = ( + fun + | MakeActive(selection) => inject(MakeActive(selection)) + ); Assistant.view( ~globals, + ~signal= + fun + | MakeActive(s) => signal(MakeActive(Scratch(s))), ~inject=action => inject(Assistant(action)), ~assistantModel, - ) + ); } : { div([]); diff --git a/src/haz3lweb/view/ScratchMode.re b/src/haz3lweb/view/ScratchMode.re index 8e922aae72..91d26c6f93 100644 --- a/src/haz3lweb/view/ScratchMode.re +++ b/src/haz3lweb/view/ScratchMode.re @@ -193,30 +193,40 @@ module Selection = { open Cursor; [@deriving (show({with_path: false}), sexp, yojson)] - type t = CellEditor.Selection.t; + type t = + | Cell(CellEditor.Selection.t) + | TextBox; let get_cursor_info = (~selection, model: Model.t): cursor(Update.t) => { - let+ ci = - CellEditor.Selection.get_cursor_info( - ~selection, - List.nth(model.scratchpads, model.current) |> snd, - ); - Update.CellAction(ci); + switch (selection) { + | Cell(s) => + let+ a = + CellEditor.Selection.get_cursor_info( + ~selection=s, + List.nth(model.scratchpads, model.current) |> snd, + ); + Update.CellAction(a); + | TextBox => empty + }; }; let handle_key_event = (~selection, ~event: Key.t, model: Model.t): option(Update.t) => - switch (event) { - | {key: D(key), sys: Mac | PC, shift: Up, meta: Down, ctrl: Up, alt: Up} - when Keyboard.is_digit(key) => - Some(Update.SwitchSlide(int_of_string(key))) - | _ => - CellEditor.Selection.handle_key_event( - ~selection, - ~event, - List.nth(model.scratchpads, model.current) |> snd, - ) - |> Option.map(x => Update.CellAction(x)) + switch (selection) { + | Cell(s) => + switch (event) { + | {key: D(key), sys: Mac | PC, shift: Up, meta: Down, ctrl: Up, alt: Up} + when Keyboard.is_digit(key) => + Some(Update.SwitchSlide(int_of_string(key))) + | _ => + CellEditor.Selection.handle_key_event( + ~selection=s, + ~event, + List.nth(model.scratchpads, model.current) |> snd, + ) + |> Option.map(x => Update.CellAction(x)) + } + | TextBox => None }; let jump_to_tile = (tile, model: Model.t): option((Update.t, t)) => @@ -224,19 +234,19 @@ module Selection = { tile, List.nth(model.scratchpads, model.current) |> snd, ) - |> Option.map(((x, y)) => (Update.CellAction(x), y)); + |> Option.map(((x, y)) => (Update.CellAction(x), Cell(y))); }; module View = { type event = - | MakeActive(CellEditor.Selection.t); + | MakeActive(Selection.t); let view = ( ~globals, ~signal: event => 'a, ~inject: Update.t => 'a, - ~selected: option(Selection.t), + ~selection: option(Selection.t), model: Model.t, ) => { ( @@ -250,9 +260,13 @@ module View = { ~globals, ~signal= fun - | MakeActive(selection) => signal(MakeActive(selection)), + | MakeActive(selection) => signal(MakeActive(Cell(selection))), ~inject=a => inject(CellAction(a)), - ~selected, + ~selected= + switch (selection) { + | Some(Selection.Cell(s)) => Some(s) + | _ => None + }, ~locked=false, List.nth(model.scratchpads, model.current) |> snd, ), diff --git a/src/haz3lweb/www/style.css b/src/haz3lweb/www/style.css index c4fd5bab25..4d20f5cfc8 100644 --- a/src/haz3lweb/www/style.css +++ b/src/haz3lweb/www/style.css @@ -98,8 +98,8 @@ select:hover { } #side-bar { - grid-row: 2 / span 1; - grid-column: 2 / span 1; + display: flex; + flex-direction: column; border-left: 0.6px solid var(--BR2); overflow-y: scroll; overflow-x: hidden; diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 5ace8b2b1e..ffbc6290f3 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -66,16 +66,18 @@ #assistant .input-container { align-items: center; - padding: 1em 0em; + margin-top: auto; + padding: 2em 0em; width: 100%; display: flex; gap: 10px; + transition: all 0.3s ease; } #assistant .message-input { flex: 1; background-color: var(--SAND); - padding: 8px; + padding: 14px; outline: none; border-radius: 3.5px; border: none; From 1f46fbe4958de6d44a6d70df5b3de712f1591779 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 19 Jan 2025 22:11:06 -0500 Subject: [PATCH 09/50] can send message and appeds to chat list in Assistant.Model --- src/haz3lweb/app/sidebar/assistant/Assistant.re | 7 ++++++- src/haz3lweb/app/sidebar/assistant/AssistantModel.re | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index 74148484b3..84fe1ce076 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -109,6 +109,11 @@ let message_input = (~signal, ~inject, ~globals: Globals.t): Node.t => { | input => Js.to_string(input##.value) }, ); + Js.Opt.case( + Dom_html.document##getElementById(Js.string("message-input")), + () => (), + el => Js.Unsafe.coerce(el)##.value := Js.string(""), + ); handle_send(message); }; @@ -128,7 +133,7 @@ let message_input = (~signal, ~inject, ~globals: Globals.t): Node.t => { (), ), div( - ~attrs=[clss(["send-button"])], + ~attrs=[clss(["send-button"]), Attr.on_click(send_message)], [ Widgets.button(~tooltip="Submit Message", Icons.send, send_message), ], diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re index db83cce8b9..0d2b8ce7fe 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -44,8 +44,7 @@ module Update = { (~settings: Settings.t, action, model: Model.t): Updated.t(Model.t) => { switch (action) { | SendMessage(message) => - print_endline(message); - Model.{chat: ["updated", "and testing"]} |> Updated.return_quiet; + Model.{chat: ["updated", "and testing"]} |> Updated.return_quiet }; }; }; From 9e688e57ae4d551401b6e4025673b4dc0520b17a Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Tue, 21 Jan 2025 11:12:58 -0500 Subject: [PATCH 10/50] adds a bit more logic for opening/closing sidebar; also begins to implement message and chat types within AssistantModel.re --- src/haz3lweb/Settings.re | 5 ++++- src/haz3lweb/app/sidebar/Sidebar.re | 6 ++++-- .../app/sidebar/assistant/Assistant.re | 4 ++-- .../app/sidebar/assistant/AssistantModel.re | 20 ++++++++++++++++--- src/haz3lweb/www/style/assistant.css | 2 +- src/haz3lweb/www/style/sidebar.css | 6 ++---- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index 67fb485c6a..641cef703c 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -196,8 +196,11 @@ module Update = { | Sidebar(SwitchWindow(windowToSwitchTo)) => { ...settings, sidebar: { + show: + !settings.sidebar.show + ? true + : settings.sidebar.window == windowToSwitchTo ? false : true, window: windowToSwitchTo, - show: true, }, } | ExplainThis(ToggleShowFeedback) => { diff --git a/src/haz3lweb/app/sidebar/Sidebar.re b/src/haz3lweb/app/sidebar/Sidebar.re index 1b9f2d4ac7..4bfb67de64 100644 --- a/src/haz3lweb/app/sidebar/Sidebar.re +++ b/src/haz3lweb/app/sidebar/Sidebar.re @@ -28,7 +28,8 @@ let explain_this_tab = (~globals: Globals.t): Node.t => { Icons.explain_this, ~tooltip, switch_explain_this, - globals.settings.sidebar.window == LanguageDocumentation, + globals.settings.sidebar.window == LanguageDocumentation + && globals.settings.sidebar.show, ), ], ); @@ -48,7 +49,8 @@ let assistant_tab = (~globals: Globals.t): Node.t => { Icons.assistant, ~tooltip, switch_assistant, - globals.settings.sidebar.window == HelpfulAssistant, + globals.settings.sidebar.window == HelpfulAssistant + && globals.settings.sidebar.show, ), ], ); diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index 84fe1ce076..d223368dd8 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -24,7 +24,7 @@ let llm_toggle = (~globals: Globals.t): Node.t => { text("Manual LLM: "), Widgets.toggle( ~tooltip, - "🕵️‍♀️", + "🔎", globals.settings.assistant.llm, toggle_llm, ), @@ -45,7 +45,7 @@ let lsp_toggle = (~globals: Globals.t): Node.t => { text("Manual LSP: "), Widgets.toggle( ~tooltip, - "🧑‍🔧", + "💬", globals.settings.assistant.lsp, toggle_lsp, ), diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re index 0d2b8ce7fe..561e9e707c 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -29,7 +29,22 @@ module Settings = { module Model = { [@deriving (show({with_path: false}), sexp, yojson)] - type t = {chat: list(string)}; + type party = + | Prompt + | Human + | LLM + | LSP; + + [@deriving (show({with_path: false}), sexp, yojson)] + type message = { + party, + msg: string, + // This id is to help group LLM/LSP chats together... helpful for knowing what to send to LLM + pass_id: int, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = {chat: list(message)}; [@deriving (show({with_path: false}), sexp, yojson)] let init: t = {chat: []}; @@ -43,8 +58,7 @@ module Update = { let update = (~settings: Settings.t, action, model: Model.t): Updated.t(Model.t) => { switch (action) { - | SendMessage(message) => - Model.{chat: ["updated", "and testing"]} |> Updated.return_quiet + | SendMessage(message) => Model.{chat: []} |> Updated.return_quiet }; }; }; diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index ffbc6290f3..1cebfac8f8 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -67,7 +67,7 @@ #assistant .input-container { align-items: center; margin-top: auto; - padding: 2em 0em; + padding: .2em 0em; width: 100%; display: flex; gap: 10px; diff --git a/src/haz3lweb/www/style/sidebar.css b/src/haz3lweb/www/style/sidebar.css index eb2c345a04..a1a84a2c20 100644 --- a/src/haz3lweb/www/style/sidebar.css +++ b/src/haz3lweb/www/style/sidebar.css @@ -58,12 +58,10 @@ } #persistent .tab:hover { - background-color: var(--T2); /* Darken tab background on hover */ + background-color: var(--T2); } -/* Optional: Add focus or active state to enhance interaction */ #persistent .tab:focus, #persistent .tab:active { - background-color: var(--T2); /* Darken a little more when tab is active */ + background-color: var(--T2); } - From 3db8632cd33c96696376db9a68cc2363b7ba796f Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Tue, 21 Jan 2025 23:52:58 -0500 Subject: [PATCH 11/50] messages now display in chat log; added a datastructure for chat log --- .../app/sidebar/assistant/Assistant.re | 64 +++++++++++++++++-- .../app/sidebar/assistant/AssistantModel.re | 29 ++++++--- src/haz3lweb/view/Page.re | 6 +- src/haz3lweb/www/style/assistant.css | 36 +++++++++++ 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index d223368dd8..9f19696664 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -66,11 +66,12 @@ let begin_chat_button = (~globals: Globals.t): Node.t => { ); }; -let end_chat_button = (~globals: Globals.t): Node.t => { +let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { let tooltip = "End Chat"; let end_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), + inject(AssistantModel.Update.EndChat), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( @@ -90,9 +91,21 @@ let settings_box = (~globals: Globals.t): Node.t => { ); }; -let message_input = (~signal, ~inject, ~globals: Globals.t): Node.t => { +let message_input = + ( + ~signal, + ~inject, + ~globals: Globals.t, + ~assistantModel: AssistantModel.Model.t, + ) + : Node.t => { let handle_send = (message: string) => { - JsUtil.log("Message sent: " ++ message); + let message: AssistantModel.Model.message = { + party: assistantModel.currSender, + content: message, + pass_id: (-1) // russ todo: implement pass ID properly + }; + JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ inject(AssistantModel.Update.SendMessage(message)), Virtual_dom.Vdom.Effect.Stop_propagation, @@ -142,7 +155,45 @@ let message_input = (~signal, ~inject, ~globals: Globals.t): Node.t => { ); }; -let view = (~globals: Globals.t, ~signal, ~inject, ~assistantModel) => { +let message_display = + ( + ~signal, + ~inject, + ~globals: Globals.t, + ~assistantModel: AssistantModel.Model.t, + ) + : Node.t => { + let message_nodes = + List.map( + (message: AssistantModel.Model.message) => { + div( + ~attrs=[ + clss(["message-container", message.party == LLM ? "llm" : "ls"]), + ], + [ + div( + ~attrs=[clss(["message-content"])], + [text(message.content)], + ), + ], + ) + }, + assistantModel.chat, + ); + div( + ~attrs=[clss(["message-display-container"])], + message_nodes + @ [message_input(~signal, ~inject, ~globals, ~assistantModel)], + ); +}; + +let view = + ( + ~globals: Globals.t, + ~signal, + ~inject, + ~assistantModel: AssistantModel.Model.t, + ) => { div( ~attrs=[Attr.id("side-bar")], [ @@ -157,13 +208,14 @@ let view = (~globals: Globals.t, ~signal, ~inject, ~assistantModel) => { [text("Agentic Assistant Chat")], ), globals.settings.assistant.ongoing_chat - ? end_chat_button(~globals) : None, + ? end_chat_button(~globals, ~inject) : None, ], ), globals.settings.assistant.ongoing_chat ? None : settings_box(~globals), globals.settings.assistant.ongoing_chat - ? message_input(~signal, ~inject, ~globals) : None, + ? message_display(~signal, ~inject, ~globals, ~assistantModel) + : None, ], ), ], diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re index 561e9e707c..4741ab6618 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -31,34 +31,45 @@ module Model = { [@deriving (show({with_path: false}), sexp, yojson)] type party = | Prompt - | Human + | Task | LLM - | LSP; + | LS; [@deriving (show({with_path: false}), sexp, yojson)] type message = { party, - msg: string, - // This id is to help group LLM/LSP chats together... helpful for knowing what to send to LLM + content: string, + // This id is to help group LLM/LS chats together... helpful for knowing what to send to LLM pass_id: int, }; [@deriving (show({with_path: false}), sexp, yojson)] - type t = {chat: list(message)}; + type t = { + chat: list(message) /*To-do: Add chat ids for saving past chats*/, + currSender: party, + }; [@deriving (show({with_path: false}), sexp, yojson)] - let init: t = {chat: []}; + let init: t = {chat: [], currSender: Task}; }; module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = - | SendMessage(string); + | SendMessage(Model.message) + | EndChat; let update = - (~settings: Settings.t, action, model: Model.t): Updated.t(Model.t) => { + (~settings: Settings.t, ~action, ~model: Model.t): Updated.t(Model.t) => { switch (action) { - | SendMessage(message) => Model.{chat: []} |> Updated.return_quiet + | SendMessage(message) => + Model.{ + chat: model.chat @ [message], + currSender: + model.currSender == LLM || model.currSender == Task ? LS : LLM, + } + |> Updated.return_quiet + | EndChat => Model.{chat: [], currSender: Task} |> Updated.return_quiet }; }; }; diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index ef30f46919..f46a02325c 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -239,7 +239,11 @@ module Update = { | Assistant(action) => let settings = globals.settings.assistant; let* assistant = - AssistantModel.Update.update(~settings, action, model.assistant); + AssistantModel.Update.update( + ~settings, + ~action, + ~model=model.assistant, + ); {...model, assistant}; | MakeActive(selection) => {...model, selection} |> Updated.return | Benchmark(Start) => diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 1cebfac8f8..56d34605bd 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -108,3 +108,39 @@ background-color: var(--T3); } +/* Message Display Container */ +#assistant .message-display-container { + display: flex; + flex-direction: column; + margin-top: auto; + overflow-y: auto; + padding: 10px; + background-color: var(--UI-Background); + gap: 10px; +} + +/* Individual message container */ +#assistant .message-container { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +#assistant .llm { + justify-content: none; +} + +#assistant .ls { + justify-content: flex-end; +} + +/* Content of the message */ +#assistant .message-content { + padding: 8px 12px; + background-color: var(--SAND); + border-radius: 12px; + font-size: 1em; + max-width: 80%; + word-wrap: break-word; +} + From 6919fe5bf74f8e0970599d9a9fd25de441103e48 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Tue, 21 Jan 2025 23:57:04 -0500 Subject: [PATCH 12/50] messages now display in chat log; added a datastructure for chat log --- src/haz3lweb/www/style/assistant.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 56d34605bd..07c5aff0ef 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -112,11 +112,11 @@ #assistant .message-display-container { display: flex; flex-direction: column; - margin-top: auto; overflow-y: auto; padding: 10px; background-color: var(--UI-Background); gap: 10px; + flex-grow: 1; } /* Individual message container */ From 3f33becad24e3aa4c136a51cb2b4b562fe576890 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Thu, 23 Jan 2025 14:20:32 -0500 Subject: [PATCH 13/50] adds resume chat button --- .../app/sidebar/assistant/Assistant.re | 56 ++++++++++++++----- .../app/sidebar/assistant/AssistantModel.re | 15 ++--- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/sidebar/assistant/Assistant.re index 9f19696664..3296a49ef5 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/sidebar/assistant/Assistant.re @@ -53,11 +53,12 @@ let lsp_toggle = (~globals: Globals.t): Node.t => { ); }; -let begin_chat_button = (~globals: Globals.t): Node.t => { - let tooltip = "Begin Chat"; +let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { + let tooltip = "Begin New Chat"; let begin_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), + inject(AssistantModel.Update.NewChat), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( @@ -66,12 +67,24 @@ let begin_chat_button = (~globals: Globals.t): Node.t => { ); }; +let resume_chat_button = (~globals: Globals.t, ~inject): Node.t => { + let tooltip = "Resume Previous Chat"; + let resume_chat = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(UpdateChatStatus))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(resume_chat)], + [Widgets.button_named(~tooltip, None, resume_chat)], + ); +}; + let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { let tooltip = "End Chat"; let end_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), - inject(AssistantModel.Update.EndChat), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( @@ -80,13 +93,14 @@ let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; -let settings_box = (~globals: Globals.t): Node.t => { +let settings_box = (~globals: Globals.t, ~inject): Node.t => { div( ~attrs=[clss(["settings-box"])], [ llm_toggle(~globals), lsp_toggle(~globals), - begin_chat_button(~globals), + begin_chat_button(~globals, ~inject), + resume_chat_button(~globals, ~inject), ], ); }; @@ -112,6 +126,10 @@ let message_input = ]); }; + let nothing = () => { + Virtual_dom.Vdom.Effect.Many([Virtual_dom.Vdom.Effect.Stop_propagation]); + }; + let send_message = _ => { let message = Js.Opt.case( @@ -129,7 +147,13 @@ let message_input = ); handle_send(message); }; - + let handle_keydown = event => { + let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); + switch (key) { + | Some("Enter") => send_message() + | _ => Virtual_dom.Vdom.Effect.Ignore + }; + }; div( ~attrs=[clss(["input-container"])], [ @@ -141,15 +165,18 @@ let message_input = Attr.on_focus(_ => signal(MakeActive(ScratchMode.Selection.TextBox)) ), + Attr.on_keydown(handle_keydown), clss(["message-input"]), ], (), ), div( - ~attrs=[clss(["send-button"]), Attr.on_click(send_message)], - [ - Widgets.button(~tooltip="Submit Message", Icons.send, send_message), + ~attrs=[ + clss(["send-button", "icon"]), + Attr.on_click(send_message), + Attr.title("Submit Message"), ], + [Icons.send], ), ], ); @@ -180,11 +207,7 @@ let message_display = }, assistantModel.chat, ); - div( - ~attrs=[clss(["message-display-container"])], - message_nodes - @ [message_input(~signal, ~inject, ~globals, ~assistantModel)], - ); + div(~attrs=[clss(["message-display-container"])], message_nodes); }; let view = @@ -212,10 +235,13 @@ let view = ], ), globals.settings.assistant.ongoing_chat - ? None : settings_box(~globals), + ? None : settings_box(~globals, ~inject), globals.settings.assistant.ongoing_chat ? message_display(~signal, ~inject, ~globals, ~assistantModel) : None, + globals.settings.assistant.ongoing_chat + ? message_input(~signal, ~inject, ~globals, ~assistantModel) + : None, ], ), ], diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re index 4741ab6618..09353b4523 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/sidebar/assistant/AssistantModel.re @@ -50,26 +50,27 @@ module Model = { }; [@deriving (show({with_path: false}), sexp, yojson)] - let init: t = {chat: [], currSender: Task}; + let init: t = {chat: [], currSender: LS}; }; module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = | SendMessage(Model.message) - | EndChat; + | NewChat; let update = (~settings: Settings.t, ~action, ~model: Model.t): Updated.t(Model.t) => { switch (action) { | SendMessage(message) => - Model.{ - chat: model.chat @ [message], - currSender: - model.currSender == LLM || model.currSender == Task ? LS : LLM, + { + Model.{ + chat: model.chat @ [message], + currSender: model.currSender == LLM ? LS : LLM, + }; } |> Updated.return_quiet - | EndChat => Model.{chat: [], currSender: Task} |> Updated.return_quiet + | NewChat => Model.{chat: [], currSender: LS} |> Updated.return_quiet }; }; }; From 8c18a8408094e4cd07b8fb04d0e2b56accf9bd53 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 25 Jan 2025 20:15:51 -0500 Subject: [PATCH 14/50] connects LLM to assistant chatbot --- src/haz3lweb/Store.re | 15 ++ .../{sidebar => }/assistant/AssistantModel.re | 52 ++++- .../AssistantView.re} | 1 - src/haz3lweb/app/assistant/Oracle.re | 36 +++ .../{sidebar => }/explainthis/ColorSteps.re | 0 .../app/{sidebar => }/explainthis/Example.re | 0 .../{sidebar => }/explainthis/ExplainThis.re | 0 .../explainthis/ExplainThisForm.re | 0 .../explainthis/ExplainThisModel.re | 0 .../explainthis/ExplainThisUpdate.re | 0 .../{sidebar => }/explainthis/data/AppExp.re | 0 .../{sidebar => }/explainthis/data/AppPat.re | 0 .../explainthis/data/ArrowTyp.re | 0 .../{sidebar => }/explainthis/data/CaseExp.re | 0 .../explainthis/data/FilterExp.re | 0 .../{sidebar => }/explainthis/data/FixFExp.re | 0 .../explainthis/data/ForallTyp.re | 0 .../explainthis/data/FunctionExp.re | 0 .../{sidebar => }/explainthis/data/HoleExp.re | 0 .../{sidebar => }/explainthis/data/HolePat.re | 0 .../explainthis/data/HoleTPat.re | 0 .../explainthis/data/HoleTemplate.re | 0 .../{sidebar => }/explainthis/data/HoleTyp.re | 0 .../{sidebar => }/explainthis/data/IfExp.re | 0 .../{sidebar => }/explainthis/data/LetExp.re | 0 .../{sidebar => }/explainthis/data/ListExp.re | 0 .../{sidebar => }/explainthis/data/ListPat.re | 0 .../{sidebar => }/explainthis/data/ListTyp.re | 0 .../{sidebar => }/explainthis/data/OpExp.re | 0 .../explainthis/data/PipelineExp.re | 0 .../{sidebar => }/explainthis/data/RecTyp.re | 0 .../{sidebar => }/explainthis/data/SeqExp.re | 0 .../{sidebar => }/explainthis/data/SumTyp.re | 0 .../explainthis/data/TerminalExp.re | 0 .../explainthis/data/TerminalPat.re | 0 .../explainthis/data/TerminalTyp.re | 0 .../{sidebar => }/explainthis/data/TestExp.re | 0 .../explainthis/data/TupleExp.re | 0 .../explainthis/data/TuplePat.re | 0 .../explainthis/data/TupleTyp.re | 0 .../explainthis/data/TyAliasExp.re | 0 .../explainthis/data/TypAnnPat.re | 0 .../explainthis/data/TypAppExp.re | 0 .../explainthis/data/TypFunctionExp.re | 0 .../explainthis/data/UndefinedExp.re | 0 .../{sidebar => }/explainthis/data/VarTPat.re | 0 src/haz3lweb/app/sidebar/assistant/Chat.re | 3 - src/haz3lweb/util/API/API.re | 161 ++++++++++++++ src/haz3lweb/util/API/OpenAI.re | 207 ++++++++++++++++++ src/haz3lweb/view/Page.re | 7 +- 50 files changed, 465 insertions(+), 17 deletions(-) rename src/haz3lweb/app/{sidebar => }/assistant/AssistantModel.re (51%) rename src/haz3lweb/app/{sidebar/assistant/Assistant.re => assistant/AssistantView.re} (99%) create mode 100644 src/haz3lweb/app/assistant/Oracle.re rename src/haz3lweb/app/{sidebar => }/explainthis/ColorSteps.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/Example.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/ExplainThis.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/ExplainThisForm.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/ExplainThisModel.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/ExplainThisUpdate.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/AppExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/AppPat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/ArrowTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/CaseExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/FilterExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/FixFExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/ForallTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/FunctionExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/HoleExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/HolePat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/HoleTPat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/HoleTemplate.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/HoleTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/IfExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/LetExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/ListExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/ListPat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/ListTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/OpExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/PipelineExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/RecTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/SeqExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/SumTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TerminalExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TerminalPat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TerminalTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TestExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TupleExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TuplePat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TupleTyp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TyAliasExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TypAnnPat.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TypAppExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/TypFunctionExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/UndefinedExp.re (100%) rename src/haz3lweb/app/{sidebar => }/explainthis/data/VarTPat.re (100%) delete mode 100644 src/haz3lweb/app/sidebar/assistant/Chat.re create mode 100644 src/haz3lweb/util/API/API.re create mode 100644 src/haz3lweb/util/API/OpenAI.re diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index 932f56f1e2..64de4bb29b 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -71,3 +71,18 @@ module F = save(data); }; }; + +// todo russ: make work with .F +module Generic = { + let prefix: string = "KEY_STORE_"; + + let full_key = (key: string): string => { + prefix ++ key; + }; + + let save = (key: string, value: string): unit => + JsUtil.set_localstore(full_key(key), value); + + let load = (key: string): option(string) => + JsUtil.get_localstore(full_key(key)); +}; diff --git a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re similarity index 51% rename from src/haz3lweb/app/sidebar/assistant/AssistantModel.re rename to src/haz3lweb/app/assistant/AssistantModel.re index 09353b4523..ca23b9d16c 100644 --- a/src/haz3lweb/app/sidebar/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -39,8 +39,6 @@ module Model = { type message = { party, content: string, - // This id is to help group LLM/LS chats together... helpful for knowing what to send to LLM - pass_id: int, }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -57,20 +55,56 @@ module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = | SendMessage(Model.message) - | NewChat; + | NewChat + | Respond(Model.message); + + let react = (response: string): t => { + // let response = response |> sanitize_response |> quote; + let response: Model.message = {party: LLM, content: response}; + Respond(response); + }; let update = - (~settings: Settings.t, ~action, ~model: Model.t): Updated.t(Model.t) => { + (~settings: Settings.t, ~action, ~model: Model.t, ~schedule_action) + : Updated.t(Model.t) => { switch (action) { | SendMessage(message) => - { - Model.{ - chat: model.chat @ [message], - currSender: model.currSender == LLM ? LS : LLM, + // todo: send API Call here + switch (message.party) { + | LS => + switch (Oracle.ask(message.content)) { + | None => print_endline("Oracle: prompt generation failed") + | Some(prompt) => + let llm = OpenAI.Azure_GPT4_0613; + let key = OpenAI.lookup_key(llm); + let params: OpenAI.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenAI.start_chat(~params, ~key, prompt, req => + switch (OpenAI.handle_chat(req)) { + | Some({content, _}) => schedule_action(react(content)) + | None => print_endline("Assistant: response parse failed") + } + ); }; + Model.{chat: model.chat @ [message], currSender: LLM} + |> Updated.return_quiet; + | _ => + Model.{ + chat: + model.chat + @ [ + { + party: LS, + content: "Message Not Sent: Waiting for LLM Response", + }, + ], + currSender: LLM, + } + |> Updated.return_quiet } - |> Updated.return_quiet | NewChat => Model.{chat: [], currSender: LS} |> Updated.return_quiet + | Respond(message) => + Model.{chat: model.chat @ [message], currSender: LS} + |> Updated.return_quiet }; }; }; diff --git a/src/haz3lweb/app/sidebar/assistant/Assistant.re b/src/haz3lweb/app/assistant/AssistantView.re similarity index 99% rename from src/haz3lweb/app/sidebar/assistant/Assistant.re rename to src/haz3lweb/app/assistant/AssistantView.re index 3296a49ef5..bf6e032c46 100644 --- a/src/haz3lweb/app/sidebar/assistant/Assistant.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -117,7 +117,6 @@ let message_input = let message: AssistantModel.Model.message = { party: assistantModel.currSender, content: message, - pass_id: (-1) // russ todo: implement pass ID properly }; JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ diff --git a/src/haz3lweb/app/assistant/Oracle.re b/src/haz3lweb/app/assistant/Oracle.re new file mode 100644 index 0000000000..f30bb013ce --- /dev/null +++ b/src/haz3lweb/app/assistant/Oracle.re @@ -0,0 +1,36 @@ +open Haz3lcore; + +let sanitize_prompt = (prompt: string): string => { + //HACK: replacement of ?? below + let prompt = Str.global_replace(Str.regexp("\\?\\?"), "", prompt); + let prompt = + if (Str.string_match(Str.regexp("^\".*\"$"), prompt, 0)) { + String.sub(prompt, 1, String.length(prompt) - 2); + } else { + prompt; + }; + prompt; +}; + +let ask = (body: string): option(OpenAI.prompt) => { + /* + let system_prompt = [ + "Respond as minimally as possible", + "Do not include a period at the end of your response", + ]; + */ + switch (String.trim(body)) { + | "" => None + | _ => + let input = + /* [OpenAI.{role: System, content: String.concat("\n", system_prompt)}] + @ */ + [{OpenAI.role: User, OpenAI.content: body}]; + Some(input); + }; +}; + +let sanitize_response: string => string = + Str.global_replace(Str.regexp("\""), "'"); + +let quote = s => "\"" ++ s ++ "\""; diff --git a/src/haz3lweb/app/sidebar/explainthis/ColorSteps.re b/src/haz3lweb/app/explainthis/ColorSteps.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/ColorSteps.re rename to src/haz3lweb/app/explainthis/ColorSteps.re diff --git a/src/haz3lweb/app/sidebar/explainthis/Example.re b/src/haz3lweb/app/explainthis/Example.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/Example.re rename to src/haz3lweb/app/explainthis/Example.re diff --git a/src/haz3lweb/app/sidebar/explainthis/ExplainThis.re b/src/haz3lweb/app/explainthis/ExplainThis.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/ExplainThis.re rename to src/haz3lweb/app/explainthis/ExplainThis.re diff --git a/src/haz3lweb/app/sidebar/explainthis/ExplainThisForm.re b/src/haz3lweb/app/explainthis/ExplainThisForm.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/ExplainThisForm.re rename to src/haz3lweb/app/explainthis/ExplainThisForm.re diff --git a/src/haz3lweb/app/sidebar/explainthis/ExplainThisModel.re b/src/haz3lweb/app/explainthis/ExplainThisModel.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/ExplainThisModel.re rename to src/haz3lweb/app/explainthis/ExplainThisModel.re diff --git a/src/haz3lweb/app/sidebar/explainthis/ExplainThisUpdate.re b/src/haz3lweb/app/explainthis/ExplainThisUpdate.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/ExplainThisUpdate.re rename to src/haz3lweb/app/explainthis/ExplainThisUpdate.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/AppExp.re b/src/haz3lweb/app/explainthis/data/AppExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/AppExp.re rename to src/haz3lweb/app/explainthis/data/AppExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/AppPat.re b/src/haz3lweb/app/explainthis/data/AppPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/AppPat.re rename to src/haz3lweb/app/explainthis/data/AppPat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/ArrowTyp.re b/src/haz3lweb/app/explainthis/data/ArrowTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/ArrowTyp.re rename to src/haz3lweb/app/explainthis/data/ArrowTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/CaseExp.re b/src/haz3lweb/app/explainthis/data/CaseExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/CaseExp.re rename to src/haz3lweb/app/explainthis/data/CaseExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/FilterExp.re b/src/haz3lweb/app/explainthis/data/FilterExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/FilterExp.re rename to src/haz3lweb/app/explainthis/data/FilterExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/FixFExp.re b/src/haz3lweb/app/explainthis/data/FixFExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/FixFExp.re rename to src/haz3lweb/app/explainthis/data/FixFExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/ForallTyp.re b/src/haz3lweb/app/explainthis/data/ForallTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/ForallTyp.re rename to src/haz3lweb/app/explainthis/data/ForallTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/FunctionExp.re b/src/haz3lweb/app/explainthis/data/FunctionExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/FunctionExp.re rename to src/haz3lweb/app/explainthis/data/FunctionExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/HoleExp.re b/src/haz3lweb/app/explainthis/data/HoleExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/HoleExp.re rename to src/haz3lweb/app/explainthis/data/HoleExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/HolePat.re b/src/haz3lweb/app/explainthis/data/HolePat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/HolePat.re rename to src/haz3lweb/app/explainthis/data/HolePat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/HoleTPat.re b/src/haz3lweb/app/explainthis/data/HoleTPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/HoleTPat.re rename to src/haz3lweb/app/explainthis/data/HoleTPat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/HoleTemplate.re b/src/haz3lweb/app/explainthis/data/HoleTemplate.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/HoleTemplate.re rename to src/haz3lweb/app/explainthis/data/HoleTemplate.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/HoleTyp.re b/src/haz3lweb/app/explainthis/data/HoleTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/HoleTyp.re rename to src/haz3lweb/app/explainthis/data/HoleTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/IfExp.re b/src/haz3lweb/app/explainthis/data/IfExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/IfExp.re rename to src/haz3lweb/app/explainthis/data/IfExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/LetExp.re b/src/haz3lweb/app/explainthis/data/LetExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/LetExp.re rename to src/haz3lweb/app/explainthis/data/LetExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/ListExp.re b/src/haz3lweb/app/explainthis/data/ListExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/ListExp.re rename to src/haz3lweb/app/explainthis/data/ListExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/ListPat.re b/src/haz3lweb/app/explainthis/data/ListPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/ListPat.re rename to src/haz3lweb/app/explainthis/data/ListPat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/ListTyp.re b/src/haz3lweb/app/explainthis/data/ListTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/ListTyp.re rename to src/haz3lweb/app/explainthis/data/ListTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/OpExp.re b/src/haz3lweb/app/explainthis/data/OpExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/OpExp.re rename to src/haz3lweb/app/explainthis/data/OpExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/PipelineExp.re b/src/haz3lweb/app/explainthis/data/PipelineExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/PipelineExp.re rename to src/haz3lweb/app/explainthis/data/PipelineExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/RecTyp.re b/src/haz3lweb/app/explainthis/data/RecTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/RecTyp.re rename to src/haz3lweb/app/explainthis/data/RecTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/SeqExp.re b/src/haz3lweb/app/explainthis/data/SeqExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/SeqExp.re rename to src/haz3lweb/app/explainthis/data/SeqExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/SumTyp.re b/src/haz3lweb/app/explainthis/data/SumTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/SumTyp.re rename to src/haz3lweb/app/explainthis/data/SumTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TerminalExp.re b/src/haz3lweb/app/explainthis/data/TerminalExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TerminalExp.re rename to src/haz3lweb/app/explainthis/data/TerminalExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TerminalPat.re b/src/haz3lweb/app/explainthis/data/TerminalPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TerminalPat.re rename to src/haz3lweb/app/explainthis/data/TerminalPat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TerminalTyp.re b/src/haz3lweb/app/explainthis/data/TerminalTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TerminalTyp.re rename to src/haz3lweb/app/explainthis/data/TerminalTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TestExp.re b/src/haz3lweb/app/explainthis/data/TestExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TestExp.re rename to src/haz3lweb/app/explainthis/data/TestExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TupleExp.re b/src/haz3lweb/app/explainthis/data/TupleExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TupleExp.re rename to src/haz3lweb/app/explainthis/data/TupleExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TuplePat.re b/src/haz3lweb/app/explainthis/data/TuplePat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TuplePat.re rename to src/haz3lweb/app/explainthis/data/TuplePat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TupleTyp.re b/src/haz3lweb/app/explainthis/data/TupleTyp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TupleTyp.re rename to src/haz3lweb/app/explainthis/data/TupleTyp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TyAliasExp.re b/src/haz3lweb/app/explainthis/data/TyAliasExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TyAliasExp.re rename to src/haz3lweb/app/explainthis/data/TyAliasExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TypAnnPat.re b/src/haz3lweb/app/explainthis/data/TypAnnPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TypAnnPat.re rename to src/haz3lweb/app/explainthis/data/TypAnnPat.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TypAppExp.re b/src/haz3lweb/app/explainthis/data/TypAppExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TypAppExp.re rename to src/haz3lweb/app/explainthis/data/TypAppExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/TypFunctionExp.re b/src/haz3lweb/app/explainthis/data/TypFunctionExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/TypFunctionExp.re rename to src/haz3lweb/app/explainthis/data/TypFunctionExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/UndefinedExp.re b/src/haz3lweb/app/explainthis/data/UndefinedExp.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/UndefinedExp.re rename to src/haz3lweb/app/explainthis/data/UndefinedExp.re diff --git a/src/haz3lweb/app/sidebar/explainthis/data/VarTPat.re b/src/haz3lweb/app/explainthis/data/VarTPat.re similarity index 100% rename from src/haz3lweb/app/sidebar/explainthis/data/VarTPat.re rename to src/haz3lweb/app/explainthis/data/VarTPat.re diff --git a/src/haz3lweb/app/sidebar/assistant/Chat.re b/src/haz3lweb/app/sidebar/assistant/Chat.re deleted file mode 100644 index de6d1860d0..0000000000 --- a/src/haz3lweb/app/sidebar/assistant/Chat.re +++ /dev/null @@ -1,3 +0,0 @@ -open Util; -open Haz3lcore; -open Web; diff --git a/src/haz3lweb/util/API/API.re b/src/haz3lweb/util/API/API.re new file mode 100644 index 0000000000..c5fa0530ef --- /dev/null +++ b/src/haz3lweb/util/API/API.re @@ -0,0 +1,161 @@ +open Js_of_ocaml; +open Util.OptUtil.Syntax; + +let opt = Util.OptUtil.and_then; + +type request = Js.t(XmlHttpRequest.xmlHttpRequest); + +type method = + | GET + | POST + | PUT + | DELETE; + +let string_of_method = + fun + | GET => "GET" + | POST => "POST" + | PUT => "PUT" + | DELETE => "DELETE"; + +module Json = { + type t = Yojson.Safe.t; + let to_string = Yojson.Safe.to_string; + let from_string = Yojson.Safe.from_string; + let bool = (json: t): option(bool) => + switch (json) { + | `Bool(b) => Some(b) + | _ => None + }; + let int = (json: t): option(int) => + switch (json) { + | `Int(n) => Some(n) + | _ => None + }; + let float = (json: t): option(float) => + switch (json) { + | `Float(f) => Some(f) + | _ => None + }; + let str = (json: t): option(string) => + switch (json) { + | `String(str) => Some(str) + | _ => None + }; + let list = (json: t): option(list(t)) => + switch (json) { + | `List(xs) => Some(xs) + | _ => None + }; + let get_kvs = (json: t): option(list((string, t))) => + switch (json) { + | `Assoc(pairs) => Some(pairs) + | _ => None + }; + let dot = (key: string, json: t): option(t) => { + let* pairs = get_kvs(json); + List.assoc_opt(key, pairs); + }; +}; + +let receive = (~debug=true, request: request): option(Json.t) => + switch (request##.readyState) { + | XmlHttpRequest.DONE => + debug ? Firebug.console##log(request##.responseText) : (); + Js.Opt.case( + request##.responseText, + () => None, + x => Some(x |> Js.to_string |> Json.from_string), + ); + | _ => None + }; + +let request = + ( + ~debug=false, + ~with_credentials=false, + ~method: method, + ~url: string, + ~headers: list((string, string))=[], + ~body: Json.t=`Null, + handler: option(Json.t) => unit, + ) + : unit => { + debug ? Yojson.Safe.pp(Format.std_formatter, body) : (); + let request = XmlHttpRequest.create(); + request##.onreadystatechange := + Js.wrap_callback(_ => handler(receive(request))); + request##.withCredentials := with_credentials |> Js.bool; + request##_open( + method |> string_of_method |> Js.string, + url |> Js.string, + true |> Js.bool, + ); + for (i in 0 to List.length(headers) - 1) { + let (key, value) = List.nth(headers, i); + request##setRequestHeader(Js.string(key), Js.string(value)); + }; + request##send(body |> Json.to_string |> Js.string |> Js.Opt.return); +}; + +let node_request = + ( + ~debug=false, + ~with_credentials=false, + ~method: method, + ~hostname: string, /* Do not include 'https://' */ + ~path: string, + ~headers: list((string, string))=[], + ~body: Json.t=`Null, + handler: option(Json.t) => unit, + ) + : unit => { + let https = Js.Unsafe.js_expr("require('https')"); + debug ? Yojson.Safe.pp(Format.std_formatter, body) : (); + let options = + Printf.sprintf( + "({hostname: \"%s\", path: \"%s\", method: \"%s\", headers: { %s } })", + hostname, + path, + string_of_method(method), + headers + |> List.map(((k, v)) => Printf.sprintf("\"%s\": \"%s\"", k, v)) + |> String.concat(","), + ); + debug ? Printf.printf("options: %s", options) : (); + let callback = + Js.wrap_callback(res => { + let data = ref(""); + res##on( + Js.string("data"), + Js.wrap_callback(chunk => + data := data^ ++ Js.to_string(chunk##toString) + ), + ); + res##on( + Js.string("end"), + Js.wrap_callback(_ => + handler( + try(Some(Json.from_string(data.contents))) { + | _ => None + }, + ) + ), + ); + }); + let req = https##request(Js.Unsafe.js_expr(options), callback); + if (with_credentials) { + req##withCredentials := Js._true; + }; + ignore( + req##on( + Js.string("error"), + Js.wrap_callback(error => { + Firebug.console##log("Error occurred:"); + Firebug.console##log(error); + }), + ), + ); + ignore(req##write(Js.string(Json.to_string(body)))); + ignore(req##end_()); +}; diff --git a/src/haz3lweb/util/API/OpenAI.re b/src/haz3lweb/util/API/OpenAI.re new file mode 100644 index 0000000000..20317699c7 --- /dev/null +++ b/src/haz3lweb/util/API/OpenAI.re @@ -0,0 +1,207 @@ +module Sexp = Sexplib.Sexp; +open API; +open Util.OptUtil.Syntax; +open Util; + +[@deriving (show({with_path: false}), sexp, yojson)] +type chat_models = + | GPT4 + | GPT3_5Turbo + | Azure_GPT4_0613 + | Azure_GPT3_5Turbo; + +[@deriving (show({with_path: false}), sexp, yojson)] +type role = + | System + | User + | Assistant + | Function; + +[@deriving (show({with_path: false}), sexp, yojson)] +type params = { + llm: chat_models, + temperature: float, + top_p: float, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type message = { + role, + content: string, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type prompt = list(message); + +[@deriving (show({with_path: false}), sexp, yojson)] +type usage = { + prompt_tokens: int, + completion_tokens: int, + total_tokens: int, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type reply = { + content: string, + usage, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +let string_of_chat_model = + fun + | GPT4 => "gpt-4" + | GPT3_5Turbo => "gpt-3.5-turbo" + | Azure_GPT4_0613 => "azure-gpt-4" + | Azure_GPT3_5Turbo => "azure-gpt-3.5-turbo"; + +let string_of_role = + fun + | System => "system" + | User => "user" + | Assistant => "assistant" + | Function => "function"; + +let default_params = {llm: Azure_GPT4_0613, temperature: 1.0, top_p: 1.0}; + +let mk_message = ({role, content}) => + `Assoc([ + ("role", `String(string_of_role(role))), + ("content", `String(content)), + ]); + +let body = (~params: params, messages: prompt): Json.t => { + `Assoc([ + ("model", `String(string_of_chat_model(params.llm))), + ("temperature", `Float(params.temperature)), + ("top_p", `Float(params.top_p)), + ("messages", `List(List.map(mk_message, messages))), + ]); +}; + +let lookup_key = (llm: chat_models) => + switch (llm) { + | Azure_GPT3_5Turbo => Store.Generic.load("AZURE") + | Azure_GPT4_0613 => Store.Generic.load("AZURE4") + | GPT3_5Turbo + | GPT4 => Store.Generic.load("OpenAI") + }; + +/* SAMPLE OPENAI CHAT RESPONSE: + { + "id":"chatcmpl-6y5167eYM6ovo5yVThXzr5CB8oVIO", + "object":"chat.completion", + "created":1679776984, + "model":"gpt-3.5-turbo-0301", + "usage":{ + "prompt_tokens":25, + "completion_tokens":1, + "total_tokens":26 + }, + "choices":[ + { + "message":{ + "role":"assistant", + "content":"576" + }, + "finish_reason":"stop", + "index":0 + } + ] + }*/ + +let chat = (~key, ~body, ~handler): unit => + switch (key) { + | None => print_endline("API: OpenAI KEY NOT FOUND") + | Some(api_key) => + print_endline("API: POSTing OpenAI request"); + request( + ~method=POST, + ~url="https://api.openai.com/v1/chat/completions", + ~headers=[ + ("Content-Type", "application/json"), + ("Authorization", "Bearer " ++ api_key), + ], + ~body, + handler, + ); + }; + +let azure_request = + (~key, ~resource, ~deployment, ~api_version, ~body, ~handler): unit => + switch (key) { + | None => print_endline("API: KEY NOT FOUND") + | Some(api_key) => + print_endline("API: POSTing Azure request"); + request( + ~method=POST, + ~url= + Printf.sprintf( + "https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", + resource, + deployment, + api_version, + ), + ~headers=[("Content-Type", "application/json"), ("api-key", api_key)], + ~body, + handler, + ); + }; + +let chat_azure35 = + azure_request( + ~resource="hazel", + ~deployment="gpt35turbo", + ~api_version="2023-05-15", + ); + +let chat_azure4 = + azure_request( + ~resource="hazel2", + ~deployment="hazel-gpt-4", + ~api_version="2023-05-15", + ); + +let start_chat = (~params, ~key, prompt: prompt, handler): unit => { + let body = body(~params, prompt); + switch (params.llm) { + | Azure_GPT3_5Turbo => chat_azure35(~key, ~body, ~handler) + | Azure_GPT4_0613 => chat_azure4(~key, ~body, ~handler) + | GPT3_5Turbo + | GPT4 => chat(~key, ~body, ~handler) + }; +}; + +let int_field = (json: Json.t, field: string) => { + let* num = Json.dot(field, json); + Json.int(num); +}; + +let of_usage = (choices: Json.t): option(usage) => { + let* prompt_tokens = int_field(choices, "prompt_tokens"); + let* completion_tokens = int_field(choices, "completion_tokens"); + let+ total_tokens = int_field(choices, "total_tokens"); + {prompt_tokens, completion_tokens, total_tokens}; +}; + +let first_message_content = (choices: Json.t): option(string) => { + let* choices = Json.list(choices); + let* hd = Util.ListUtil.hd_opt(choices); + let* message = Json.dot("message", hd); + let* content = Json.dot("content", message); + Json.str(content); +}; + +let handle_chat = (~db=ignore, response: option(Json.t)): option(reply) => { + db("OpenAI: Chat response:"); + Option.map(r => r |> Json.to_string |> db, response) |> ignore; + let* json = response; + let* choices = Json.dot("choices", json); + let* usage = Json.dot("usage", json); + let* content = first_message_content(choices); + let+ usage = of_usage(usage); + {content, usage}; +}; + +let add_to_prompt = (prompt, ~assistant, ~user): prompt => + prompt + @ [{role: Assistant, content: assistant}, {role: User, content: user}]; diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index f46a02325c..4d7a8d9c8b 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -240,9 +240,8 @@ module Update = { let settings = globals.settings.assistant; let* assistant = AssistantModel.Update.update( - ~settings, - ~action, - ~model=model.assistant, + ~settings, ~action, ~model=model.assistant, ~schedule_action=a => + schedule_action(Assistant(a)) ); {...model, assistant}; | MakeActive(selection) => {...model, selection} |> Updated.return @@ -503,7 +502,7 @@ module View = { fun | MakeActive(selection) => inject(MakeActive(selection)) ); - Assistant.view( + AssistantView.view( ~globals, ~signal= fun From d3f5f95d78ec44412621f829b7b3c74671f40029 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 25 Jan 2025 20:30:06 -0500 Subject: [PATCH 15/50] adds ... while waiting for LLM response --- src/haz3lweb/app/assistant/AssistantModel.re | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index ca23b9d16c..5bdbff50b9 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -85,25 +85,17 @@ module Update = { } ); }; - Model.{chat: model.chat @ [message], currSender: LLM} - |> Updated.return_quiet; - | _ => + let await_llm_response: Model.message = {party: LLM, content: "..."}; Model.{ - chat: - model.chat - @ [ - { - party: LS, - content: "Message Not Sent: Waiting for LLM Response", - }, - ], + chat: model.chat @ [message, await_llm_response], currSender: LLM, } - |> Updated.return_quiet + |> Updated.return_quiet; + | _ => Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet } | NewChat => Model.{chat: [], currSender: LS} |> Updated.return_quiet | Respond(message) => - Model.{chat: model.chat @ [message], currSender: LS} + Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} |> Updated.return_quiet }; }; From e37cf9bdafbe85fbac11c84860b8e45df2929a52 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 25 Jan 2025 20:34:38 -0500 Subject: [PATCH 16/50] fixing build failure --- src/haz3lweb/app/assistant/AssistantView.re | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index bf6e032c46..363771cf6c 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -125,10 +125,6 @@ let message_input = ]); }; - let nothing = () => { - Virtual_dom.Vdom.Effect.Many([Virtual_dom.Vdom.Effect.Stop_propagation]); - }; - let send_message = _ => { let message = Js.Opt.case( From bf94572588837b294d03cf57230e184cfcf211e7 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 26 Jan 2025 09:05:11 -0500 Subject: [PATCH 17/50] made loading dots prettier --- src/haz3lweb/app/assistant/AssistantView.re | 39 +++++++++++++++------ src/haz3lweb/www/style/assistant.css | 32 +++++++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 363771cf6c..680d4ed227 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -177,6 +177,17 @@ let message_input = ); }; +let loading_dots = () => { + div( + ~attrs=[clss(["loading-dots"])], + [ + div(~attrs=[clss(["dot", "dot1"])], []), + div(~attrs=[clss(["dot", "dot2"])], []), + div(~attrs=[clss(["dot", "dot3"])], []), + ], + ); +}; + let message_display = ( ~signal, @@ -188,17 +199,23 @@ let message_display = let message_nodes = List.map( (message: AssistantModel.Model.message) => { - div( - ~attrs=[ - clss(["message-container", message.party == LLM ? "llm" : "ls"]), - ], - [ - div( - ~attrs=[clss(["message-content"])], - [text(message.content)], - ), - ], - ) + print_endline(message.content); + message.content == "..." + ? loading_dots() + : div( + ~attrs=[ + clss([ + "message-container", + message.party == LLM ? "llm" : "ls", + ]), + ], + [ + div( + ~attrs=[clss(["message-content"])], + [text(message.content)], + ), + ], + ); }, assistantModel.chat, ); diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 07c5aff0ef..e42c1bab1b 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -144,3 +144,35 @@ word-wrap: break-word; } +#assistant .loading-dots { + display: flex; + align-items: center; + gap: 6px; + } + + #assistant .dot { + width: 8px; + height: 8px; + background-color: var(--STONE); + border-radius: 50%; + animation: bounce 1.4s infinite ease-in-out both; + } + + #assistant .dot1 { + animation-delay: -0.32s; + } + + #assistant .dot2 { + animation-delay: -0.16s; + } + + @keyframes bounce { + 0%, 80%, 100% { + transform: scale(0.7); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } + } \ No newline at end of file From 55c55afc9df2368b77ad9fb587d75e3fe4a9f5fc Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 26 Jan 2025 09:19:11 -0500 Subject: [PATCH 18/50] fixes bug with dots; removes autocomplete from input box --- src/haz3lweb/app/assistant/AssistantView.re | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 680d4ed227..0b5c47a8c7 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -54,7 +54,7 @@ let lsp_toggle = (~globals: Globals.t): Node.t => { }; let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { - let tooltip = "Begin New Chat"; + let tooltip = "New Chat"; let begin_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), @@ -68,7 +68,7 @@ let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { }; let resume_chat_button = (~globals: Globals.t, ~inject): Node.t => { - let tooltip = "Resume Previous Chat"; + let tooltip = "Previous Chat"; let resume_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), @@ -157,6 +157,7 @@ let message_input = Attr.id("message-input"), Attr.placeholder("Type a message..."), Attr.type_("text"), + Attr.property("autocomplete", Js.Unsafe.inject("off")), Attr.on_focus(_ => signal(MakeActive(ScratchMode.Selection.TextBox)) ), @@ -200,7 +201,7 @@ let message_display = List.map( (message: AssistantModel.Model.message) => { print_endline(message.content); - message.content == "..." + message.content == "..." && message.party == LLM ? loading_dots() : div( ~attrs=[ From a76dd16fae152ce1f69f3504a0be6de2e35ea595 Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Sun, 26 Jan 2025 15:55:49 -0500 Subject: [PATCH 19/50] disabled sending a message while the LLM is forming a response --- src/haz3lweb/app/assistant/AssistantView.re | 32 ++++++++++++++------- src/haz3lweb/www/style/assistant.css | 12 ++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 0b5c47a8c7..e05fc811ec 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -144,8 +144,9 @@ let message_input = }; let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key) { - | Some("Enter") => send_message() + switch (key, ListUtil.last_opt(assistantModel.chat)) { + | (_, Some({party: LLM, content: "..."})) => Virtual_dom.Vdom.Effect.Ignore + | (Some("Enter"), _) => send_message() | _ => Virtual_dom.Vdom.Effect.Ignore }; }; @@ -166,14 +167,25 @@ let message_input = ], (), ), - div( - ~attrs=[ - clss(["send-button", "icon"]), - Attr.on_click(send_message), - Attr.title("Submit Message"), - ], - [Icons.send], - ), + switch (ListUtil.last_opt(assistantModel.chat)) { + | Some({party: LLM, content: "..."}) => + div( + ~attrs=[ + clss(["disabled-send-button", "icon"]), + Attr.title("Submitting Message Disabled"), + ], + [Icons.thin_x], + ) + | _ => + div( + ~attrs=[ + clss(["send-button", "icon"]), + Attr.on_click(send_message), + Attr.title("Submit Message"), + ], + [Icons.send], + ) + }, ], ); }; diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index e42c1bab1b..13aacd1c48 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -88,6 +88,18 @@ box-shadow: 0px 0px 1px var(--SHADOW), 0 0 0 1px var(--SHADOW); } +#assistant .send-button-disabled { + background-color: var(--SAND); + color: var(--STONE); + padding: .65em .75em; + border: 0 solid var(--SAND); + border-radius: .5em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + pointer-events: none; +} + #assistant .send-button { background-color: var(--SAND); color: var(--STONE); From e718ccad3029168f282d493f2aed69ac2da2956c Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Fri, 31 Jan 2025 13:31:10 -0500 Subject: [PATCH 20/50] added cut/copy/pasting in message box --- src/haz3lweb/view/Page.re | 92 ++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 4d7a8d9c8b..8cbeae9055 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -347,44 +347,74 @@ module View = { JsUtil.focus_clipboard_shim(); Effect.Ignore; }), - Attr.on_copy(_ => { - JsUtil.copy( - (cursor.selected_text |> Option.value(~default=() => ""))(), - ); + Attr.on_copy(evt => { + let target = Js.Opt.to_option(evt##.target); + switch (target) { + | Some(el) => + let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); + switch (elId) { + | Some("message-input") => () + | _ => + JsUtil.copy( + (cursor.selected_text |> Option.value(~default=() => ""))(), + ) + }; + | None => () + }; Effect.Ignore; }), - Attr.on_cut(_ => { - JsUtil.copy( - (cursor.selected_text |> Option.value(~default=() => ""))(), - ); - Option.map( - inject, - Selection.handle_key_event( - ~selection=Some(model.selection), - ~event= - Key.{ - key: D("Delete"), - sys: Os.is_mac^ ? Mac : PC, - shift: Up, - meta: Up, - ctrl: Up, - alt: Up, - }, - model, - ), - ) - |> Option.value(~default=Effect.Ignore); + Attr.on_cut(evt => { + let target = Js.Opt.to_option(evt##.target); + switch (target) { + | Some(el) => + let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); + switch (elId) { + | Some("message-input") => Effect.Ignore + | _ => + JsUtil.copy( + (cursor.selected_text |> Option.value(~default=() => ""))(), + ); + Option.map( + inject, + Selection.handle_key_event( + ~selection=Some(model.selection), + ~event= + Key.{ + key: D("Delete"), + sys: Os.is_mac^ ? Mac : PC, + shift: Up, + meta: Up, + ctrl: Up, + alt: Up, + }, + model, + ), + ) + |> Option.value(~default=Effect.Ignore); + }; + | None => Effect.Ignore + }; }), ] @ [ Attr.on_paste(evt => { - let pasted_text = - Js.to_string(evt##.clipboardData##getData(Js.string("text"))) - |> Str.global_replace(Str.regexp("\n[ ]*"), "\n"); - Dom.preventDefault(evt); - switch (cursor.editor_action(Paste(pasted_text))) { + let target = Js.Opt.to_option(evt##.target); + switch (target) { + | Some(el) => + let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); + switch (elId) { + | Some("message-input") => Effect.Ignore + | _ => + let pasted_text = + Js.to_string(evt##.clipboardData##getData(Js.string("text"))) + |> Str.global_replace(Str.regexp("\n[ ]*"), "\n"); + Dom.preventDefault(evt); + switch (cursor.editor_action(Paste(pasted_text))) { + | None => Effect.Ignore + | Some(action) => inject(Editors(action)) + }; + }; | None => Effect.Ignore - | Some(action) => inject(Editors(action)) }; }), ]; From 3f6c2106d84e114e267cf5fcacf84c5e84365bd9 Mon Sep 17 00:00:00 2001 From: disconcision Date: Fri, 31 Jan 2025 22:20:50 -0500 Subject: [PATCH 21/50] port over internals relevany to collating prompt from llama-lsp-lookahead --- src/haz3lweb/app/assistant/ChatLSP.re | 867 ++++++++++++++++++++++++++ src/haz3lweb/debug/DebugConsole.re | 19 + 2 files changed, 886 insertions(+) create mode 100644 src/haz3lweb/app/assistant/ChatLSP.re diff --git a/src/haz3lweb/app/assistant/ChatLSP.re b/src/haz3lweb/app/assistant/ChatLSP.re new file mode 100644 index 0000000000..a02cfe5380 --- /dev/null +++ b/src/haz3lweb/app/assistant/ChatLSP.re @@ -0,0 +1,867 @@ +open Util; +open OptUtil.Syntax; +open Haz3lcore; + +[@deriving (show({with_path: false}), yojson, sexp)] +type parse_error = option(string); + +[@deriving (show({with_path: false}), yojson, sexp)] +type static_errors = list(string); + +[@deriving (show({with_path: false}), yojson, sexp)] +type error_report = + | ParseError(string) + | StaticErrors(static_errors) + | NoErrors; + +[@deriving (show({with_path: false}), yojson, sexp)] +type round_report = { + reply: OpenAI.reply, + error_report, +}; + +type samples = list((string, string, string)); + +[@deriving (show({with_path: false}), sexp, yojson)] +type filler_options = { + params: OpenAI.params, + instructions: bool, + syntax_notes: bool, + num_examples: int, + expected_type: bool, + error_rounds_max: int, + relevant_ctx: bool, + rag: option(string), +}; + +let filler_options_init: filler_options = { + params: OpenAI.default_params, + instructions: true, + syntax_notes: true, + num_examples: 9, + expected_type: true, + error_rounds_max: 2, + relevant_ctx: true, + rag: None, +}; + +let pretty_print_seg = + (~holes: option(string)=Some(""), segment: Segment.t): string => + Printer.to_rows( + ~holes, + ~measured= + segment + |> ZipperBase.MapPiece.of_segment( + ProjectorPerform.Update.remove_any_projector, + ) + |> Measured.of_segment(_, Id.Map.empty), + ~caret=None, + ~indent=" ", + ~segment, + ) + |> String.concat("\n"); + +module ErrorPrint = { + /* + ERRORS TODO: + make multihole an error (say something about ap) + do a completeness check + */ + + let prn = Printf.sprintf; + + let common_error: Info.error_common => string = + fun + // | NoType(MultiError) => + // /* NOTE: possible cause explanation actually helps. + // e.g. when generating + // "if i == index then (description, not(done)) else (description, done)" + // it would tend not to parethesize the argument to not + // */ + // prn( + // "Incomplete syntax (possible cause: remember that function application is c-style and requires parentheses around the argument)", + // ) + + | NoType(BadToken(token)) => prn("\"%s\" isn't a valid token", token) + | NoType(BadTrivAp(ty)) => + prn( + "Function argument type \"%s\" inconsistent with ()", + Typ.pretty_print(ty), + ) + | Inconsistent(WithArrow(ty)) => + prn("type %s is not consistent with arrow type", Typ.pretty_print(ty)) + | NoType(FreeConstructor(_name)) => prn("Constructor is not defined") + | Inconsistent(Internal(tys)) => + prn( + "Expecting branches to have consistent types but got types: %s", + List.map(Typ.pretty_print, tys) |> String.concat(", "), + ) + | Inconsistent(Expectation({ana, syn})) => + prn( + "Expecting type %s but got inconsistent type %s", + Typ.pretty_print(ana), + Typ.pretty_print(syn), + ); + + let exp_error: Info.error_exp => string = + fun + | FreeVariable(name) => "Variable " ++ name ++ " is not bound" + | InexhaustiveMatch(_) => "TODO: Match is not exhaustive" + | UnusedDeferral => "TODO: Unused deferral" + | BadPartialAp(_) => "TODO: Bad partial app" + | Common(error) => common_error(error); + + let pat_error: Info.error_pat => string = + fun + | ExpectedConstructor => "Expected a constructor" + | Redundant(_) => "TODO: Redundant" + | Common(error) => common_error(error); + + let typ_error: Info.error_typ => string = + fun + | FreeTypeVariable(name) => prn("Type variable %s is not bound", name) + | BadToken(token) => prn("\"%s\" isn't a valid type token", token) + | WantConstructorFoundAp => "Expected a constructor, found application" + | WantConstructorFoundType(ty) => + prn("Expected a constructor, found type %s", Typ.pretty_print(ty)) + | WantTypeFoundAp => "Constructor application must be in sum" + | DuplicateConstructor(name) => + prn("Constructor %s already used in this sum", name); + + let tpat_error: Info.error_tpat => string = + fun + | NotAVar(_) => "TODO: Not a valid type name" + | ShadowsType(name, _source) => "TODO: Can't shadow type " ++ name; + + let string_of: Info.error => string = + fun + | Exp(error) => exp_error(error) + | Pat(error) => pat_error(error) + | Typ(error) => typ_error(error) + | TPat(error) => tpat_error(error); + + let format_error = (term, error) => + prn("Error in term:\n %s\nNature of error: %s", term, error); + + let term_string_of: Info.t => string = + fun + | InfoExp({term, _}) => + term + |> ExpToSegment.exp_to_pretty( + ~settings= + ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), + ) + |> pretty_print_seg(~holes=None) + | InfoPat({term, _}) => + term + |> ExpToSegment.pat_to_pretty( + ~settings= + ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), + ) + |> pretty_print_seg(~holes=None) + | InfoTyp({term, _}) => Typ.pretty_print(term) + | InfoTPat({term, _}) => + term + |> ExpToSegment.tpat_to_pretty( + ~settings= + ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), + ) + |> pretty_print_seg(~holes=None) + | Secondary(_) => "TODO"; + + let collect_static = (info_map: Statics.Map.t): list(string) => { + let errors = + Id.Map.fold( + (_id, info: Info.t, acc) => + switch (Info.error_of(info)) { + | None => acc + | Some(_) => [info] @ acc + }, + info_map, + [], + ); + let errors = List.sort_uniq(compare, errors); + List.filter_map( + info => + switch (Info.error_of(info)) { + | None => None + | Some(error) => + let term = term_string_of(info); + Some(format_error(term, string_of(error))); + }, + errors, + ); + }; +}; + +module RelevantType = { + let expected_ty_no_lookup = (mode: Mode.t): Typ.t => { + switch (mode) { + | Ana(ty) => ty + | SynFun => + Typ.fresh( + Arrow(Typ.fresh(Unknown(Internal)), Typ.fresh(Unknown(Internal))), + ) + | Syn + | SynTypFun => Typ.fresh(Unknown(SynSwitch)) + }; + }; + + let expected_ty = (~ctx, mode: Mode.t): Typ.t => { + switch (mode) { + | Ana({term: Var(name), _}) when Ctx.lookup_alias(ctx, name) != None => + let ty_expanded = Ctx.lookup_alias(ctx, name) |> Option.get; + ty_expanded; + | _ => expected_ty_no_lookup(mode) + }; + }; + + let format_def = (alias: string, ty: Typ.t): string => { + Printf.sprintf("type %s = %s in", alias, Typ.pretty_print(ty)); + }; + + let subst_if_rec = ((name: string, ty: Typ.t)) => { + switch (ty) { + | {term: Rec(name', ty'), _} => ( + name, + Typ.subst(Typ.fresh(Var(name)), name', ty'), + ) + | _ => (name, ty) + }; + }; + + let rec get_vars = (ty: Typ.t): list(string) => + switch (ty.term) { + | Int + | Float + | Bool + | String + | Unknown(_) => [] + | Var(x) => [x] + | Arrow(ty1, ty2) => get_vars(ty1) @ get_vars(ty2) + | Prod(tys) => ListUtil.flat_map(get_vars, tys) + | Sum(sm) => + List.concat_map( + fun + | ConstructorMap.BadEntry(_) => [] + | Variant(_, _, None) => [] + | Variant(_, _, Some(typ)) => get_vars(typ), + sm, + ) + | Rec({term: Var(x), _}, ty) => + /* Remove recursive type references */ + get_vars(ty) |> List.filter((x': string) => x' != x) + | Rec(_, ty) => get_vars(ty) + | List(ty) => get_vars(ty) + | Parens(ty) => get_vars(ty) + | Forall({term: Var(x), _}, ty) => + get_vars(ty) |> List.filter((x': string) => x' != x) + | Forall(_, ty) => get_vars(ty) + | Ap(ty1, ty2) => get_vars(ty1) @ get_vars(ty2) + }; + + let rec collect_aliases_deep = + (ctx: Ctx.t, ty: Typ.t): list((string, Typ.t)) => { + let ty_vars = get_vars(ty); + let defs = + ListUtil.flat_map( + var => + switch (Ctx.lookup_alias(ctx, var)) { + | Some(ty) => [(var, ty)] + | None => [(var, Typ.fresh(Unknown(Internal)))] + }, + ty_vars, + ) + |> List.sort_uniq(((x, _), (y, _)) => compare(x, y)); + let rec_calls = + ListUtil.flat_map( + ((_, ty')) => collect_aliases_deep(ctx, ty'), + defs, + ); + rec_calls @ defs; + }; + + let collate_aliases = (ctx, expected_ty'): option(string) => { + let defs = + collect_aliases_deep(ctx, expected_ty') + |> Util.ListUtil.dedup + |> List.map(subst_if_rec) + |> List.map(((alias, ty)) => format_def(alias, ty)); + switch (defs) { + | [] => None + | _ => Some(defs |> String.concat("\n")) + }; + }; + + let expected = (~ctx, mode: Mode.t): string => { + /* + TODO(andrew): maybe include more than just the immediate type. + like for example, when inside a fn(s), include + argument types. + like basically to benefit maximally from included type info, + want to make sure we're including the full expansion of any type + we might want to either case on or construct. + expected type should mostly(?) give us the latter, + but not always the former + */ + let prefix = "# The expected type of the hole ?? is: "; + switch (mode) { + | Ana(ty) => + let defs = + switch (collate_aliases(ctx, expected_ty_no_lookup(mode))) { + | Some(defs) => + "# The following type definitions are likely relevant: #\n" ++ defs + | None => "\n" + }; + prefix + ++ "a type consistent with " + ++ Typ.pretty_print(ty) + ++ " #\n" + ++ defs; + | SynFun => + prefix + ++ "a type consistent with " + ++ Typ.pretty_print( + Typ.fresh( + Arrow( + Typ.fresh(Unknown(Internal)), + Typ.fresh(Unknown(Internal)), + ), + ), + ) + ++ " #" + | Syn => prefix ++ "any type #" + | _ => "Not applicable" + }; + }; +}; + +module RelevantCtx = { + [@deriving (show({with_path: false}), sexp, yojson)] + type filtered_entry = { + name: string, + typ: Typ.t, + matched_type: Typ.t, + depth: int, + }; + + let is_list_unk = (ty: Typ.t) => + switch (ty.term) { + | List({term: Unknown(_), _}) => true + | _ => false + }; + + let is_base = (ty: Typ.t): bool => + switch (ty.term) { + | Int + | Float + | Bool + | String => true + | _ => false + }; + + let returns_base = (ty: Typ.t) => + switch (ty.term) { + | Arrow(_, ty) => is_base(ty) + | _ => false + }; + + /* Calculates the total number of nodes (compound + and leaf) in the type tree. */ + let rec num_nodes = (ty: Typ.t): int => { + switch (ty.term) { + | Int + | Float + | Bool + | String + | Unknown(_) => 1 + | Var(_) => 1 + | Arrow(t1, t2) => 1 + num_nodes(t1) + num_nodes(t2) + | Prod(tys) => + 1 + List.fold_left((acc, ty) => acc + num_nodes(ty), 0, tys) + | Sum(sm) => + 1 + + List.fold_left( + (acc, variant) => + switch (variant) { + | ConstructorMap.BadEntry(_) => acc + | Variant(_, _, ty) => + acc + Util.OptUtil.get(() => 0, Option.map(num_nodes, ty)) + }, + 0, + sm, + ) + | Rec(_, ty) => 1 + num_nodes(ty) + | List(ty) => 1 + num_nodes(ty) + | Parens(ty) => 1 + num_nodes(ty) + | Forall(_, ty) => 1 + num_nodes(ty) + | Ap(ty1, ty2) => 1 + num_nodes(ty1) + num_nodes(ty2) + }; + }; + + let rec count_unknowns = (ty: Typ.t): int => + switch (ty.term) { + | Unknown(_) => 1 + | Int + | Float + | Bool + | String + | Var(_) => 0 + | Arrow(t1, t2) => count_unknowns(t1) + count_unknowns(t2) + | Prod(tys) => + List.fold_left((acc, ty) => acc + count_unknowns(ty), 0, tys) + | Sum(sm) => + List.fold_left( + (acc, variant) => + switch (variant) { + | ConstructorMap.BadEntry(_) => acc + | Variant(_, _, ty) => + acc + Util.OptUtil.get(() => 0, Option.map(count_unknowns, ty)) + }, + 0, + sm, + ) + | Rec(_, ty) => count_unknowns(ty) + | List(ty) => count_unknowns(ty) + | Parens(ty) => count_unknowns(ty) + | Forall(_, ty) => count_unknowns(ty) + | Ap(ty1, ty2) => count_unknowns(ty1) + count_unknowns(ty2) + }; + + let rec contains_sum_or_var = (ty: Typ.t): bool => + switch (ty.term) { + | Int + | Float + | Bool + | String + | Unknown(_) => false + | Var("Option") => false //TODO(andrew): hack for LSP + | Var(_) + | Sum(_) => true + | Arrow(t1, t2) => contains_sum_or_var(t1) || contains_sum_or_var(t2) + | Prod(tys) => List.exists(contains_sum_or_var, tys) + | Rec(_, ty) => contains_sum_or_var(ty) + | List(ty) => contains_sum_or_var(ty) + | Parens(ty) => contains_sum_or_var(ty) + | Forall(_, ty) => contains_sum_or_var(ty) + | Ap(ty1, ty2) => contains_sum_or_var(ty1) || contains_sum_or_var(ty2) + }; + + /* Returns the ratio of type nodes which are the Unknown + constructor. Must recurse and gather results from composite nodes */ + let unknown_ratio = (ty: Typ.t): float => { + let total = float_of_int(num_nodes(ty)); + let unknowns = float_of_int(count_unknowns(ty)); + (total -. unknowns) /. total; + }; + + let score_type = (ty: Typ.t) => { + let unk_ratio = unknown_ratio(ty); + is_base(ty) ? 0.8 : unk_ratio; + }; + + let take_up_to_n = (n, xs) => + switch (Util.ListUtil.split_n_opt(n, xs)) { + | Some((xs, _)) => xs + | None => xs + }; + + let format_def = (name: string, ty: Typ.t) => + Printf.sprintf("let %s: %s = in", name, Typ.pretty_print(ty)); + + let filter_ctx = (ctx: Ctx.t, ty_expect: Typ.t): list(filtered_entry) => + List.filter_map( + fun + | Ctx.VarEntry({typ, name, _}) + when Typ.is_consistent(ctx, ty_expect, typ) => + Some({name, typ, depth: 0, matched_type: typ}) + | Ctx.VarEntry({typ: {term: Arrow(_, return_ty), _} as typ, name, _}) + when Typ.is_consistent(ctx, ty_expect, return_ty) => + Some({name, typ, matched_type: return_ty, depth: 1}) + | Ctx.VarEntry({ + typ: {term: Arrow(_, {term: Arrow(_, return_ty), _}), _} as typ, + name, + _, + }) + when Typ.is_consistent(ctx, ty_expect, return_ty) => + Some({name, typ, matched_type: return_ty, depth: 2}) + | _ => None, + ctx, + ); + + let str = (ctx: Ctx.t, mode: Mode.t): string => { + let primary_goal: Typ.t = + RelevantType.expected_ty(~ctx, mode) |> Typ.normalize(ctx); + let secondary_targets = + switch (primary_goal.term) { + | Arrow(_source, target) => + let terts = + switch (target.term) { + | Prod(ts) => ts + | _ => [] + }; + [target] @ terts; + | _ => [] + }; + print_endline("primary_goal: " ++ Typ.pretty_print(primary_goal)); + print_endline( + "secondary_targets: " + ++ String.concat(",", List.map(Typ.pretty_print, secondary_targets)), + ); + let primary_entries = filter_ctx(ctx, primary_goal); + let secondary_entries = + List.concat(List.map(filter_ctx(ctx, _), secondary_targets)); + let combined_entries = + secondary_entries + @ primary_entries + |> Util.ListUtil.dedup + |> List.sort((t1, t2) => + compare(score_type(t2.matched_type), score_type(t1.matched_type)) + ) + |> List.filter(entry => contains_sum_or_var(entry.typ)); + let entries = + combined_entries + |> take_up_to_n(8) + |> List.map(({name, typ, _}) => format_def(name, typ)) + |> String.concat("\n"); + "# Consider using these variables relevant to the expected type: #\n" + ++ entries; + }; +}; + +let mk_user_message = + ( + ~expected_ty: option(string), + ~relevant_ctx: option(string), + sketch: string, + ) + : string => + //TODO: proper JSON construction + "{\n" + ++ String.concat( + ",\n", + List.filter_map( + Fun.id, + [ + Some("sketch: " ++ sketch), + Option.map(Printf.sprintf("expected_ty: %s"), expected_ty), + Option.map(Printf.sprintf("relevant_ctx:\n %s"), relevant_ctx), + ], + ), + ) + ++ ",\n}"; + +module Samples = { + let samples = [ + ( + {| +let List.length: [(String, Bool)]-> Int = + fun xs -> + ?? end in +|}, + RelevantType.expected(Ana(Typ.fresh(Int)), ~ctx=[]), + {| +case xs +| [] => 0 +| _::xs => 1 + List.length(xs)|}, + ), + ( + {| +let List.mapi: ((Int, Bool) -> Bool, [Bool]) -> [Bool]= + fun f, xs -> + let go: (Int, [Bool])-> [Bool] = fun idx, xs -> + ?? end in + go(0, xs) in +|}, + RelevantType.expected( + Ana(Typ.fresh(List(Typ.fresh(Bool)))), + ~ctx=[], + ), + {| +case xs +| [] => [] +| hd::tl => f(idx, hd)::go(idx + 1, tl) +|}, + ), + ( + {| +type Container = + + Pod(Bool) + + CapsuleCluster(Int, Int) in +let total_capacity: Container -> Int = + ?? +in +|}, + RelevantType.expected( + Ana( + Typ.fresh(Arrow(Typ.fresh(Var("Container")), Typ.fresh(Int))), + ), + ~ctx=[], + ), + {| +fun c -> + case c + | Pod(b) => if !b && true then 1 else 0 + | CapsuleCluster(x, y) => x * y + end +|}, + ), + ( + "let f = ?? in f(5)", + RelevantType.expected(Syn, ~ctx=[]), + "fun x:Int -> ??", + ), + ( + {|let triple = (4, 8, true) in +let (_, y, condition) = triple in +let get: Option -> Int = +fun maybe_num -> + case maybe_num + | Some(x) => ?? + | None => if !condition then 0 else y + 1 end in|}, + RelevantType.expected(Ana(Typ.fresh(Int)), ~ctx=[]), + "x", + ), + ( + "let num_or_zero = fun maybe_num ->\n case maybe_num\n | Some(num) => ?? \n| None => 0 end in", + RelevantType.expected(Syn, ~ctx=[]), + "num", + ), + ( + "let merge_sort: [Int]->[Int] =\n??\nin\nmerge_sort([4,1,3,7,2])", + RelevantType.expected( + Ana( + Typ.fresh( + Arrow( + Typ.fresh(List(Typ.fresh(Int))), + Typ.fresh(List(Typ.fresh(Int))), + ), + ), + ), + ~ctx=[], + ), + "fun list ->\nlet split: [Int]->([Int],[Int]) = fun left, right -> ?\nin\nlet merge: ([Int],[Int])->[Int]= ?\nin\nlet merge_sort_helper: [Int]->[Int]= ?\nin\nmerge_sort_helper(list)", + ), + ( + "type MenuItem =\n+ Breakfast(Int, Int)\n+ Lunch(Float)\nin\nlet per_lunch_unit = 0.95 in\nlet price: MenuItem-> Float = fun m ->\ncase m\n| Breakfast(x, y) => ??\n| Lunch(f) => f *. per_lunch_unit\nend\nin price(Breakfast(1,2))/.3.", + RelevantType.expected(Ana(Typ.fresh(Var("MenuItem"))), ~ctx=[]), + "fun m ->\ncase m\n| Breakfast(x, y) => ??\n| Lunch(f) => f *. per_lunch_unit\nend", + ), + ( + {| +let List.merge: (( , )->Bool,[ ], [ ]) -> [ ] = fun cmp,left, right -> +case left, right +| [], _ => right +| _, [] => left +| h1::t1, h2::t2 => +if cmp(h1, h2) +then h1 :: List.merge(cmp, t1, right) +else h2 :: List.merge(cmp,left, t2) +end +in + +let List.sort: ((?, ?) -> Bool, [?]) -> [?] = +fun cmp, list -> +let merge_sort_helper: [?] -> [?] = fun l -> +case l +| [] => ? +| [x] => [x] +| _ => ?? +end +in merge_sort_helper(list) +in +test 2 == List.nth(List.sort(fun a, b -> a + switch (Util.ListUtil.split_n_opt(num_examples, samples)) { + | Some(samples) => + samples |> fst |> List.map(((s, t, u)) => (s, Some(t), u)) + | None => [] + }; + + let mk = (num_examples: int): list(OpenAI.message) => + Util.ListUtil.flat_map( + ((sketch, expected_ty, completion)): list(OpenAI.message) => + [ + { + role: User, + content: + mk_user_message(sketch, ~expected_ty, ~relevant_ctx=None), + }, + {role: Assistant, content: completion}, + ], + get(num_examples, samples), + ); +}; + +module SystemPrompt = { + let main_prompt = [ + "CODE COMPLETION INSTRUCTIONS:", + "- Reply with a functional, idiomatic replacement for the program hole marked '??' in the provided program sketch", + "- Reply only with a single replacement term for the unqiue distinguished hole marked '??'", + "- Reply only with code", + "- DO NOT suggest more replacements for other holes in the sketch (marked '?'), or implicit holes", + "- DO NOT include the program sketch in your reply", + "- DO NOT include a period at the end of your response and DO NOT use markdown", + ]; + + let hazel_syntax_notes = [ + "HAZEL SYNTAX NOTES:", + "- Hazel uses C-style function application syntax, with parenthesis around comma-separated arguments", + "- Function application is ALWAYS written using parentheses and commas: use 'function(arg1, arg2)'. DO NOT just use spaces between the function name and arguments.", + "- Function parameters are ALWAYS commas separated: 'fun arg1, arg2 -> '. DO NOT use spaces to separate function arguments.", + "- There is no dot accessor notation for tuples; DO NOT use tuple.field. use pattern matching for destructuring: let (field, _) = tuple in ...", + "- The following ARE NOT Hazel keywords. DO NOT use these keywords: switch, with, of, rec. ALWAYS omit these keywords", + "- Pattern matching is ALWAYS written a 'case ... end' expression. Cases MUST END in an 'end' keyword. DO NOT USE any other keyword besides 'case' to do pattern matching. DO NOT USE a 'with' or 'of' keyword with 'case', just start the list of rules. Pattern matching rules use syntax '| pattern => expression'. Note the '=>' arrow.", + "- The ONLY way to define a named function is by using a function expression nested in a let expression like 'let = fun -> in { + let system_prompt = + String.concat( + "\n", + (instructions ? main_prompt : []) + @ (syntax_notes ? hazel_syntax_notes : []), + ); + OpenAI.[{role: System, content: system_prompt}] + @ Samples.mk(num_examples); + }; +}; + +module ErrorRound = { + let get_top_level_errs = (init_ctx, mode, top_ci: Info.exp) => { + let self: Self.t = + switch (top_ci) { + | {self, _} => + switch (Self.typ_of_exp(init_ctx, self)) { + | None => Just(Typ.fresh(Unknown(Internal))) + | Some(ty) => Just(ty) + } + }; + let status = Info.status_common(init_ctx, mode, self); + switch (status) { + | InHole(Inconsistent(Expectation({ana, syn}))) => [ + "The suggested filling has the wrong expected type: expected " + ++ Typ.pretty_print(ana) + ++ ", but got " + ++ Typ.pretty_print(syn) + ++ ".", + ] + | _ => [] + }; + }; + + let get_parse_errs = (filling: string): Result.t(Zipper.t, string) => + switch (Printer.zipper_of_string(filling)) { + | None => Error("Undocumented parse error, no feedback available") + | Some(filling_z) => + //TODO(andrew): for syntax errors, also collect bad syntax eg % operator + switch ( + filling_z.backpack + |> List.map((s: Selection.t) => + Printer.of_segment(~holes=None, s.content) + ) + ) { + | [_, ..._] as orphans => + Error( + "The parser has detected the following unmatched delimiters:. The presence of a '=>' in the list likely indicates that a '->' was mistakingly used in a case expression: " + ++ String.concat(", ", orphans), + ) + | [] => Ok(filling_z) + } + }; + + let mk_round_report = (~init_ctx, ~mode, reply: OpenAI.reply): round_report => + switch (get_parse_errs(reply.content)) { + | Error(err) => {reply, error_report: ParseError(err)} + | Ok(filling_z) => + let (top_ci, info_map) = + Statics.uexp_to_info_map( + ~ctx=init_ctx, + ~ancestors=[], + MakeTerm.from_zip_for_sem(filling_z).term, + Id.Map.empty, + ); + let static_errs = + get_top_level_errs(init_ctx, mode, top_ci) + @ ErrorPrint.collect_static(info_map); + if (List.length(static_errs) == 0) { + {reply, error_report: NoErrors}; + } else { + {reply, error_report: StaticErrors(static_errs)}; + }; + }; + + let mk = + (~init_ctx: Ctx.t, ~mode: Mode.t, reply: OpenAI.reply) + : (error_report, string) => { + //TODO(andrew): this is implictly specialized for exp only + let wrap = (intro, errs) => + [intro] + @ errs + @ [ + "Please try to address the error(s) by updating your previous code suggestion", + "Please respond ONLY with the update suggestion", + ] + |> String.concat("\n"); + let error_report = mk_round_report(~init_ctx, ~mode, reply).error_report; + let str = + switch (error_report) { + | NoErrors => "" + | ParseError(err) => wrap("The following parse error occured:", [err]) + | StaticErrors(errs) => + wrap("The following static errors were discovered:", errs) + }; + (error_report, str); + }; +}; + +module InitPrompt = { + let mk_msg = + ( + {expected_type, relevant_ctx, _}: filler_options, + ci: Info.t, + sketch: Segment.t, + ) + : option(string) => { + //TODO: Proper errors + let* mode = + switch (ci) { + | InfoExp({mode, _}) => Some(mode) + | InfoPat({mode, _}) => Some(mode) + | _ => None + }; + let sketch = pretty_print_seg(~holes=Some("?"), sketch); + let+ () = String.trim(sketch) == "" ? None : Some(); + let ctx_at_caret = Info.ctx_of(ci); + let expected_ty = + expected_type + ? Some(RelevantType.expected(~ctx=ctx_at_caret, mode)) : None; + let relevant_ctx = + relevant_ctx ? Some(RelevantCtx.str(ctx_at_caret, mode)) : None; + mk_user_message(sketch, ~expected_ty, ~relevant_ctx); + }; + + let mk = + (filler_options: filler_options, ci: Info.t, sketch: Segment.t) + : option(OpenAI.prompt) => { + let+ user_message = mk_msg(filler_options, ci, sketch); + SystemPrompt.mk(filler_options) @ [{role: User, content: user_message}]; + }; +}; diff --git a/src/haz3lweb/debug/DebugConsole.re b/src/haz3lweb/debug/DebugConsole.re index 47e0883cc8..c455f8411c 100644 --- a/src/haz3lweb/debug/DebugConsole.re +++ b/src/haz3lweb/debug/DebugConsole.re @@ -33,6 +33,25 @@ let print = }; | None => print("DEBUG: No indicated index") }; + | "F9" => + Util.OptUtil.Syntax.( + switch ( + { + let* index = Indicated.index(zipper); + let* ci = Id.Map.find_opt(index, map); + let sketch_seg = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, zipper); + ChatLSP.InitPrompt.mk_msg( + ChatLSP.filler_options_init, + ci, + sketch_seg, + ); + } + ) { + | None => print_endline("prompt generation failed") + | Some(prompt) => print_endline(prompt) + } + ) | _ => print("DEBUG: No action for key: " ++ key) }; }; From 5f3e5086b48bfd907c3ef588db5a7cd7ae767cc3 Mon Sep 17 00:00:00 2001 From: disconcision Date: Sat, 1 Feb 2025 15:04:32 -0500 Subject: [PATCH 22/50] cleanup, expose error report in debug console --- src/haz3lweb/app/assistant/ChatLSP.re | 584 ++++++++++++-------------- src/haz3lweb/debug/DebugConsole.re | 27 +- 2 files changed, 298 insertions(+), 313 deletions(-) diff --git a/src/haz3lweb/app/assistant/ChatLSP.re b/src/haz3lweb/app/assistant/ChatLSP.re index a02cfe5380..cee933056c 100644 --- a/src/haz3lweb/app/assistant/ChatLSP.re +++ b/src/haz3lweb/app/assistant/ChatLSP.re @@ -2,76 +2,84 @@ open Util; open OptUtil.Syntax; open Haz3lcore; -[@deriving (show({with_path: false}), yojson, sexp)] -type parse_error = option(string); - -[@deriving (show({with_path: false}), yojson, sexp)] -type static_errors = list(string); - -[@deriving (show({with_path: false}), yojson, sexp)] -type error_report = - | ParseError(string) - | StaticErrors(static_errors) - | NoErrors; - -[@deriving (show({with_path: false}), yojson, sexp)] -type round_report = { - reply: OpenAI.reply, - error_report, -}; +let prn = Printf.sprintf; + +let statics_of_exp_zipper = + (init_ctx: Ctx.t, z: Zipper.t): (Info.exp, Statics.Map.t) => + Statics.uexp_to_info_map( + ~ctx=init_ctx, + ~ancestors=[], + MakeTerm.from_zip_for_sem(z).term, + Id.Map.empty, + ); + +module Options = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + params: OpenAI.params, + instructions: bool, + syntax_notes: bool, + num_examples: int, + expected_type: bool, + relevant_ctx: bool, + error_rounds_max: int, + }; -type samples = list((string, string, string)); - -[@deriving (show({with_path: false}), sexp, yojson)] -type filler_options = { - params: OpenAI.params, - instructions: bool, - syntax_notes: bool, - num_examples: int, - expected_type: bool, - error_rounds_max: int, - relevant_ctx: bool, - rag: option(string), + let init: t = { + params: OpenAI.default_params, + instructions: true, + syntax_notes: true, + num_examples: 9, + expected_type: true, + relevant_ctx: true, + error_rounds_max: 2, + }; }; -let filler_options_init: filler_options = { - params: OpenAI.default_params, - instructions: true, - syntax_notes: true, - num_examples: 9, - expected_type: true, - error_rounds_max: 2, - relevant_ctx: true, - rag: None, -}; +module Print = { + let seg = (~holes: option(string)=Some(""), segment: Segment.t): string => { + let segment = + ZipperBase.MapPiece.of_segment( + ProjectorPerform.Update.remove_any_projector, + segment, + ); + Printer.to_rows( + ~holes, + ~measured=Measured.of_segment(segment, Id.Map.empty), + ~caret=None, + ~indent=" ", + ~segment, + ) + |> String.concat("\n"); + }; -let pretty_print_seg = - (~holes: option(string)=Some(""), segment: Segment.t): string => - Printer.to_rows( - ~holes, - ~measured= - segment - |> ZipperBase.MapPiece.of_segment( - ProjectorPerform.Update.remove_any_projector, - ) - |> Measured.of_segment(_, Id.Map.empty), - ~caret=None, - ~indent=" ", - ~segment, - ) - |> String.concat("\n"); + let term = (term: Term.Any.t): string => { + let settings = + ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off); + term |> ExpToSegment.any_to_pretty(~settings) |> seg(~holes=None); + }; + + let typ = (ty: Typ.t): string => + //TODO: make sure that the ExpToSegment pretty printing fully subsumes Typ.pretty_print + //Typ.pretty_print(ty); + term(Typ(ty)); +}; module ErrorPrint = { - /* - ERRORS TODO: - make multihole an error (say something about ap) - do a completeness check - */ + [@deriving (show({with_path: false}), yojson, sexp)] + type t = + | ParseError(string) + | StaticErrors(list(string)) + | NoErrors; - let prn = Printf.sprintf; + /* TODO: + Better errors for more broken programs: Completeness/formedness checks / multihole errors + Contextualize errors with line numbers (would need to add them to the sketch), or + including surrounding syntax, or inlining an error representation into a provided sketch */ let common_error: Info.error_common => string = fun + // TODO: This error class doesn't seem to exist anymore, not sure what happens to multiholes now // | NoType(MultiError) => // /* NOTE: possible cause explanation actually helps. // e.g. when generating @@ -81,40 +89,39 @@ module ErrorPrint = { // prn( // "Incomplete syntax (possible cause: remember that function application is c-style and requires parentheses around the argument)", // ) - | NoType(BadToken(token)) => prn("\"%s\" isn't a valid token", token) | NoType(BadTrivAp(ty)) => prn( "Function argument type \"%s\" inconsistent with ()", - Typ.pretty_print(ty), + Print.typ(ty), ) | Inconsistent(WithArrow(ty)) => - prn("type %s is not consistent with arrow type", Typ.pretty_print(ty)) + prn("type %s is not consistent with arrow type", Print.typ(ty)) | NoType(FreeConstructor(_name)) => prn("Constructor is not defined") | Inconsistent(Internal(tys)) => prn( "Expecting branches to have consistent types but got types: %s", - List.map(Typ.pretty_print, tys) |> String.concat(", "), + List.map(Print.typ, tys) |> String.concat(", "), ) | Inconsistent(Expectation({ana, syn})) => prn( "Expecting type %s but got inconsistent type %s", - Typ.pretty_print(ana), - Typ.pretty_print(syn), + Print.typ(ana), + Print.typ(syn), ); let exp_error: Info.error_exp => string = fun | FreeVariable(name) => "Variable " ++ name ++ " is not bound" - | InexhaustiveMatch(_) => "TODO: Match is not exhaustive" - | UnusedDeferral => "TODO: Unused deferral" - | BadPartialAp(_) => "TODO: Bad partial app" + | InexhaustiveMatch(_) => "Match is not exhaustive" //TODO: elaborate + | UnusedDeferral => "Unused deferral" //TODO: better message + | BadPartialAp(_) => "Bad partial application" //TODO: elaborate | Common(error) => common_error(error); let pat_error: Info.error_pat => string = fun | ExpectedConstructor => "Expected a constructor" - | Redundant(_) => "TODO: Redundant" + | Redundant(_) => "Redundant" //TODO: elaborate | Common(error) => common_error(error); let typ_error: Info.error_typ => string = @@ -123,15 +130,15 @@ module ErrorPrint = { | BadToken(token) => prn("\"%s\" isn't a valid type token", token) | WantConstructorFoundAp => "Expected a constructor, found application" | WantConstructorFoundType(ty) => - prn("Expected a constructor, found type %s", Typ.pretty_print(ty)) + prn("Expected a constructor, found type %s", Print.typ(ty)) | WantTypeFoundAp => "Constructor application must be in sum" | DuplicateConstructor(name) => prn("Constructor %s already used in this sum", name); let tpat_error: Info.error_tpat => string = fun - | NotAVar(_) => "TODO: Not a valid type name" - | ShadowsType(name, _source) => "TODO: Can't shadow type " ++ name; + | NotAVar(_) => "Not a valid type name" //TODO: elaborate + | ShadowsType(name, _source) => "Can't shadow type " ++ name; //TODO: elaborate let string_of: Info.error => string = fun @@ -145,82 +152,124 @@ module ErrorPrint = { let term_string_of: Info.t => string = fun - | InfoExp({term, _}) => - term - |> ExpToSegment.exp_to_pretty( - ~settings= - ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), - ) - |> pretty_print_seg(~holes=None) - | InfoPat({term, _}) => - term - |> ExpToSegment.pat_to_pretty( - ~settings= - ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), - ) - |> pretty_print_seg(~holes=None) - | InfoTyp({term, _}) => Typ.pretty_print(term) - | InfoTPat({term, _}) => - term - |> ExpToSegment.tpat_to_pretty( - ~settings= - ExpToSegment.Settings.of_core(~inline=false, CoreSettings.off), - ) - |> pretty_print_seg(~holes=None) - | Secondary(_) => "TODO"; + | InfoExp({term, _}) => Print.term(Exp(term)) + | InfoPat({term, _}) => Print.term(Pat(term)) + | InfoTyp({term, _}) => Print.term(Typ(term)) + | InfoTPat({term, _}) => Print.term(TPat(term)) + | Secondary(_) => failwith("ChatLSP: term_string_of: Secondary"); let collect_static = (info_map: Statics.Map.t): list(string) => { - let errors = - Id.Map.fold( - (_id, info: Info.t, acc) => - switch (Info.error_of(info)) { - | None => acc - | Some(_) => [info] @ acc - }, - info_map, - [], - ); - let errors = List.sort_uniq(compare, errors); - List.filter_map( - info => + Id.Map.fold( + (_id, info: Info.t, acc) => switch (Info.error_of(info)) { - | None => None - | Some(error) => - let term = term_string_of(info); - Some(format_error(term, string_of(error))); + | None => acc + | Some(_) => [info] @ acc }, - errors, - ); + info_map, + [], + ) + |> List.sort_uniq(compare) + |> List.filter_map(info => + switch (Info.error_of(info)) { + | None => None + | Some(error) => + let term = term_string_of(info); + Some(format_error(term, string_of(error))); + } + ); }; -}; -module RelevantType = { - let expected_ty_no_lookup = (mode: Mode.t): Typ.t => { - switch (mode) { - | Ana(ty) => ty - | SynFun => - Typ.fresh( - Arrow(Typ.fresh(Unknown(Internal)), Typ.fresh(Unknown(Internal))), - ) - | Syn - | SynTypFun => Typ.fresh(Unknown(SynSwitch)) + let get_top_level_errs = (init_ctx, mode, top_ci: Info.exp) => { + let self: Self.t = + switch (top_ci) { + | {self, _} => + switch (Self.typ_of_exp(init_ctx, self)) { + | None => Just(Typ.fresh(Unknown(Internal))) + | Some(ty) => Just(ty) + } + }; + let status = Info.status_common(init_ctx, mode, self); + switch (status) { + | InHole(Inconsistent(Expectation({ana, syn}))) => [ + "The suggested completion has the wrong expected type: expected " + ++ Print.typ(ana) + ++ ", but got " + ++ Print.typ(syn) + ++ ".", + ] + | _ => [] }; }; + let get_parse_errs = (completion: string): Result.t(Zipper.t, string) => + switch (Printer.zipper_of_string(completion)) { + | None => Error("Undocumented parse error, no feedback available") + | Some(completion_z) => + //TODO: For syntax errors, also collect bad syntax eg % operator + switch ( + completion_z.backpack + |> List.map((s: Selection.t) => + Printer.of_segment(~holes=None, s.content) + ) + ) { + | [_, ..._] as orphans => + Error( + "The parser has detected the following unmatched delimiters:. The presence of a '=>' in the list likely indicates that a '->' was mistakingly used in a case expression: " + ++ String.concat(", ", orphans), + ) + | [] => Ok(completion_z) + } + }; + + let mk_errors = (~init_ctx, ~mode, reply: string): t => + switch (get_parse_errs(reply)) { + | Error(err) => ParseError(err) + | Ok(completion_z) => + //TODO: This is implictly specialized for expressions only + let (top_ci, info_map) = statics_of_exp_zipper(init_ctx, completion_z); + let static_errs = + get_top_level_errs(init_ctx, mode, top_ci) @ collect_static(info_map); + if (List.length(static_errs) == 0) { + NoErrors; + } else { + StaticErrors(static_errs); + }; + }; + + let mk = (~init_ctx: Ctx.t, ~mode: Mode.t, reply: string): option(string) => { + let wrap = (intro, errs) => + [intro] + @ errs + @ [ + "Please try to address the error(s) by updating your previous code suggestion", + "Please respond ONLY with the update suggestion", + ] + |> String.concat("\n"); + let error_report = mk_errors(~init_ctx, ~mode, reply); + switch (error_report) { + | NoErrors => None + | ParseError(err) => + Some(wrap("The following parse error occured:", [err])) + | StaticErrors(errs) => + Some(wrap("The following static errors were discovered:", errs)) + }; + }; +}; + +module RelevantType = { let expected_ty = (~ctx, mode: Mode.t): Typ.t => { switch (mode) { | Ana({term: Var(name), _}) when Ctx.lookup_alias(ctx, name) != None => let ty_expanded = Ctx.lookup_alias(ctx, name) |> Option.get; ty_expanded; - | _ => expected_ty_no_lookup(mode) + | _ => Mode.ty_of(mode) }; }; - let format_def = (alias: string, ty: Typ.t): string => { - Printf.sprintf("type %s = %s in", alias, Typ.pretty_print(ty)); - }; + let format_def = (alias: string, ty: Typ.t): string => + prn("type %s = %s in", alias, Print.typ(ty)); - let subst_if_rec = ((name: string, ty: Typ.t)) => { + let subst_if_rec = ((name: string, ty: Typ.t)): (string, Typ.t) => { switch (ty) { | {term: Rec(name', ty'), _} => ( name, @@ -281,7 +330,7 @@ module RelevantType = { rec_calls @ defs; }; - let collate_aliases = (ctx, expected_ty'): option(string) => { + let collate_aliases = (ctx: Ctx.t, expected_ty': Typ.t): option(string) => { let defs = collect_aliases_deep(ctx, expected_ty') |> Util.ListUtil.dedup @@ -293,35 +342,27 @@ module RelevantType = { }; }; - let expected = (~ctx, mode: Mode.t): string => { - /* - TODO(andrew): maybe include more than just the immediate type. - like for example, when inside a fn(s), include - argument types. - like basically to benefit maximally from included type info, - want to make sure we're including the full expansion of any type - we might want to either case on or construct. - expected type should mostly(?) give us the latter, - but not always the former - */ + let expected = (~ctx: Ctx.t, mode: Mode.t): string => { + /* TODO: Maybe include more than just the immediate type. + * like for example, when inside a fn(s), include argument types. + * Like basically to benefit maximally from included type info, + * want to make sure we're including the full expansion of any type + * we might want to either case on or construct. Rxpected type should + * mostly(?) give us the latter, but not always the former. */ let prefix = "# The expected type of the hole ?? is: "; switch (mode) { | Ana(ty) => let defs = - switch (collate_aliases(ctx, expected_ty_no_lookup(mode))) { + switch (collate_aliases(ctx, Mode.ty_of(mode))) { | Some(defs) => "# The following type definitions are likely relevant: #\n" ++ defs | None => "\n" }; - prefix - ++ "a type consistent with " - ++ Typ.pretty_print(ty) - ++ " #\n" - ++ defs; + prefix ++ "a type consistent with " ++ Print.typ(ty) ++ " #\n" ++ defs; | SynFun => prefix ++ "a type consistent with " - ++ Typ.pretty_print( + ++ Print.typ( Typ.fresh( Arrow( Typ.fresh(Unknown(Internal)), @@ -345,7 +386,16 @@ module RelevantCtx = { depth: int, }; - let is_list_unk = (ty: Typ.t) => + /* TODO: For all functions on types, we want to makse sure we're + * normalizing first where appropriate (replacing type aliases with + * their definitions). Where it's always appropriate, internalize it + * into the relevant function; otherwise, list it as a precondition */ + + /* TODO: Some of the functions below were hastily updated to dev. + * The new cases of Typ (Parens, Forsll, Ap) especially should be + * double-checked */ + + let is_list_unk = (ty: Typ.t): bool => switch (ty.term) { | List({term: Unknown(_), _}) => true | _ => false @@ -360,7 +410,7 @@ module RelevantCtx = { | _ => false }; - let returns_base = (ty: Typ.t) => + let returns_base = (ty: Typ.t): bool => switch (ty.term) { | Arrow(_, ty) => is_base(ty) | _ => false @@ -435,7 +485,7 @@ module RelevantCtx = { | Bool | String | Unknown(_) => false - | Var("Option") => false //TODO(andrew): hack for LSP + | Var("Option") => false //TODO: hack for LSP | Var(_) | Sum(_) => true | Arrow(t1, t2) => contains_sum_or_var(t1) || contains_sum_or_var(t2) @@ -455,19 +505,19 @@ module RelevantCtx = { (total -. unknowns) /. total; }; - let score_type = (ty: Typ.t) => { + let score_type = (ty: Typ.t): float => { let unk_ratio = unknown_ratio(ty); is_base(ty) ? 0.8 : unk_ratio; }; - let take_up_to_n = (n, xs) => + let take_up_to_n = (n: int, xs: list('a)): list('a) => switch (Util.ListUtil.split_n_opt(n, xs)) { | Some((xs, _)) => xs | None => xs }; let format_def = (name: string, ty: Typ.t) => - Printf.sprintf("let %s: %s = in", name, Typ.pretty_print(ty)); + prn("let %s: %s = in", name, Print.typ(ty)); let filter_ctx = (ctx: Ctx.t, ty_expect: Typ.t): list(filtered_entry) => List.filter_map( @@ -503,11 +553,6 @@ module RelevantCtx = { [target] @ terts; | _ => [] }; - print_endline("primary_goal: " ++ Typ.pretty_print(primary_goal)); - print_endline( - "secondary_targets: " - ++ String.concat(",", List.map(Typ.pretty_print, secondary_targets)), - ); let primary_entries = filter_ctx(ctx, primary_goal); let secondary_entries = List.concat(List.map(filter_ctx(ctx, _), secondary_targets)); @@ -529,30 +574,11 @@ module RelevantCtx = { }; }; -let mk_user_message = - ( - ~expected_ty: option(string), - ~relevant_ctx: option(string), - sketch: string, - ) - : string => - //TODO: proper JSON construction - "{\n" - ++ String.concat( - ",\n", - List.filter_map( - Fun.id, - [ - Some("sketch: " ++ sketch), - Option.map(Printf.sprintf("expected_ty: %s"), expected_ty), - Option.map(Printf.sprintf("relevant_ctx:\n %s"), relevant_ctx), - ], - ), - ) - ++ ",\n}"; - module Samples = { - let samples = [ + type t = list((string, string, string)); + + //TODO: I think some of these examples are syntactically incorrect + let samples: t = [ ( {| let List.length: [(String, Bool)]-> Int = @@ -684,26 +710,12 @@ List.merge(cmp, merge_sort_helper(left), merge_sort_helper(right)) ), ]; - let get = (num_examples, samples) => + let get = (num_examples: int) => switch (Util.ListUtil.split_n_opt(num_examples, samples)) { | Some(samples) => samples |> fst |> List.map(((s, t, u)) => (s, Some(t), u)) | None => [] }; - - let mk = (num_examples: int): list(OpenAI.message) => - Util.ListUtil.flat_map( - ((sketch, expected_ty, completion)): list(OpenAI.message) => - [ - { - role: User, - content: - mk_user_message(sketch, ~expected_ty, ~relevant_ctx=None), - }, - {role: Assistant, content: completion}, - ], - get(num_examples, samples), - ); }; module SystemPrompt = { @@ -730,112 +742,40 @@ module SystemPrompt = { "- Format the code with proper linebreaks", ]; - let mk = - ({instructions, syntax_notes, num_examples, _}: filler_options) - : list(OpenAI.message) => { - let system_prompt = - String.concat( - "\n", - (instructions ? main_prompt : []) - @ (syntax_notes ? hazel_syntax_notes : []), - ); - OpenAI.[{role: System, content: system_prompt}] - @ Samples.mk(num_examples); - }; -}; - -module ErrorRound = { - let get_top_level_errs = (init_ctx, mode, top_ci: Info.exp) => { - let self: Self.t = - switch (top_ci) { - | {self, _} => - switch (Self.typ_of_exp(init_ctx, self)) { - | None => Just(Typ.fresh(Unknown(Internal))) - | Some(ty) => Just(ty) - } - }; - let status = Info.status_common(init_ctx, mode, self); - switch (status) { - | InHole(Inconsistent(Expectation({ana, syn}))) => [ - "The suggested filling has the wrong expected type: expected " - ++ Typ.pretty_print(ana) - ++ ", but got " - ++ Typ.pretty_print(syn) - ++ ".", - ] - | _ => [] - }; - }; - - let get_parse_errs = (filling: string): Result.t(Zipper.t, string) => - switch (Printer.zipper_of_string(filling)) { - | None => Error("Undocumented parse error, no feedback available") - | Some(filling_z) => - //TODO(andrew): for syntax errors, also collect bad syntax eg % operator - switch ( - filling_z.backpack - |> List.map((s: Selection.t) => - Printer.of_segment(~holes=None, s.content) - ) - ) { - | [_, ..._] as orphans => - Error( - "The parser has detected the following unmatched delimiters:. The presence of a '=>' in the list likely indicates that a '->' was mistakingly used in a case expression: " - ++ String.concat(", ", orphans), - ) - | [] => Ok(filling_z) - } - }; - - let mk_round_report = (~init_ctx, ~mode, reply: OpenAI.reply): round_report => - switch (get_parse_errs(reply.content)) { - | Error(err) => {reply, error_report: ParseError(err)} - | Ok(filling_z) => - let (top_ci, info_map) = - Statics.uexp_to_info_map( - ~ctx=init_ctx, - ~ancestors=[], - MakeTerm.from_zip_for_sem(filling_z).term, - Id.Map.empty, - ); - let static_errs = - get_top_level_errs(init_ctx, mode, top_ci) - @ ErrorPrint.collect_static(info_map); - if (List.length(static_errs) == 0) { - {reply, error_report: NoErrors}; - } else { - {reply, error_report: StaticErrors(static_errs)}; - }; - }; - - let mk = - (~init_ctx: Ctx.t, ~mode: Mode.t, reply: OpenAI.reply) - : (error_report, string) => { - //TODO(andrew): this is implictly specialized for exp only - let wrap = (intro, errs) => - [intro] - @ errs - @ [ - "Please try to address the error(s) by updating your previous code suggestion", - "Please respond ONLY with the update suggestion", - ] - |> String.concat("\n"); - let error_report = mk_round_report(~init_ctx, ~mode, reply).error_report; - let str = - switch (error_report) { - | NoErrors => "" - | ParseError(err) => wrap("The following parse error occured:", [err]) - | StaticErrors(errs) => - wrap("The following static errors were discovered:", errs) - }; - (error_report, str); - }; + let mk = ({instructions, syntax_notes, _}: Options.t): string => + String.concat( + "\n", + (instructions ? main_prompt : []) + @ (syntax_notes ? hazel_syntax_notes : []), + ); }; -module InitPrompt = { - let mk_msg = +module Prompt = { + //TODO: Build JSON instead of string + let mk_user_message = + ( + ~expected_ty: option(string), + ~relevant_ctx: option(string), + sketch: string, + ) + : string => + "{\n" + ++ String.concat( + ",\n", + List.filter_map( + Fun.id, + [ + Some("sketch: " ++ sketch), + Option.map(Printf.sprintf("expected_ty: %s"), expected_ty), + Option.map(Printf.sprintf("relevant_ctx:\n %s"), relevant_ctx), + ], + ), + ) + ++ ",\n}"; + + let static_context = ( - {expected_type, relevant_ctx, _}: filler_options, + {expected_type, relevant_ctx, _}: Options.t, ci: Info.t, sketch: Segment.t, ) @@ -847,7 +787,7 @@ module InitPrompt = { | InfoPat({mode, _}) => Some(mode) | _ => None }; - let sketch = pretty_print_seg(~holes=Some("?"), sketch); + let sketch = Print.seg(~holes=Some("?"), sketch); let+ () = String.trim(sketch) == "" ? None : Some(); let ctx_at_caret = Info.ctx_of(ci); let expected_ty = @@ -858,10 +798,40 @@ module InitPrompt = { mk_user_message(sketch, ~expected_ty, ~relevant_ctx); }; - let mk = - (filler_options: filler_options, ci: Info.t, sketch: Segment.t) + let samples = (num_examples: int): list(OpenAI.message) => + Util.ListUtil.flat_map( + ((sketch, expected_ty, completion)): list(OpenAI.message) => + [ + { + role: User, + content: + mk_user_message(sketch, ~expected_ty, ~relevant_ctx=None), + }, + {role: Assistant, content: completion}, + ], + Samples.get(num_examples), + ); + + let mk_init = + (options: Options.t, ci: Info.t, sketch: Segment.t) : option(OpenAI.prompt) => { - let+ user_message = mk_msg(filler_options, ci, sketch); - SystemPrompt.mk(filler_options) @ [{role: User, content: user_message}]; + let+ user_message = static_context(options, ci, sketch); + OpenAI.[{role: System, content: SystemPrompt.mk(options)}] + @ samples(options.num_examples) + @ [{role: User, content: user_message}]; + }; + + let mk_error = (ci: Info.t, reply: OpenAI.reply): option(string) => { + /* TODO: This should maybe take whole JSON convo + * so far and return an appended version */ + //TODO: Proper errors + let* mode = + switch (ci) { + | InfoExp({mode, _}) => Some(mode) + | InfoPat({mode, _}) => Some(mode) + | _ => None + }; + let init_ctx = Info.ctx_of(ci); + ErrorPrint.mk(~init_ctx, ~mode, reply.content); }; }; diff --git a/src/haz3lweb/debug/DebugConsole.re b/src/haz3lweb/debug/DebugConsole.re index c455f8411c..d85860eaec 100644 --- a/src/haz3lweb/debug/DebugConsole.re +++ b/src/haz3lweb/debug/DebugConsole.re @@ -34,6 +34,7 @@ let print = | None => print("DEBUG: No indicated index") }; | "F9" => + print_endline("STATIC CONTEXT AT CURSOR:"); Util.OptUtil.Syntax.( switch ( { @@ -41,17 +42,31 @@ let print = let* ci = Id.Map.find_opt(index, map); let sketch_seg = Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, zipper); - ChatLSP.InitPrompt.mk_msg( - ChatLSP.filler_options_init, - ci, - sketch_seg, - ); + ChatLSP.Prompt.static_context(ChatLSP.Options.init, ci, sketch_seg); } ) { | None => print_endline("prompt generation failed") | Some(prompt) => print_endline(prompt) } - ) + ); + | "F10" => + print_endline("WHOLE PROGRAM ERROR REPORT:"); + switch ( + { + let whole_program_str = + zipper + |> Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true) + |> ChatLSP.Print.seg; + ChatLSP.ErrorPrint.mk( + ~init_ctx=Builtins.ctx_init, + ~mode=Syn, + whole_program_str, + ); + } + ) { + | None => print_endline("error reply generation failed") + | Some(prompt) => print_endline(prompt) + }; | _ => print("DEBUG: No action for key: " ++ key) }; }; From e0af80d6fc6069987c8c650cb7be1277248f3d13 Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Sat, 8 Feb 2025 17:47:57 -0500 Subject: [PATCH 23/50] tested injecting editor statics. WIP --- src/haz3lweb/app/assistant/AssistantModel.re | 33 +++++++++++++++++++- src/haz3lweb/debug/DebugConsole.re | 8 +++-- src/haz3lweb/view/Page.re | 12 ++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 5bdbff50b9..0710621d03 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -55,6 +55,7 @@ module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = | SendMessage(Model.message) + | SendSketch | NewChat | Respond(Model.message); @@ -65,7 +66,13 @@ module Update = { }; let update = - (~settings: Settings.t, ~action, ~model: Model.t, ~schedule_action) + ( + ~settings: Settings.t, + ~action, + ~editor: CellEditor.Model.t, + ~model: Model.t, + ~schedule_action, + ) : Updated.t(Model.t) => { switch (action) { | SendMessage(message) => @@ -97,6 +104,30 @@ module Update = { | Respond(message) => Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} |> Updated.return_quiet + | SendSketch => + Util.OptUtil.Syntax.( + switch ( + { + let* index = Indicated.index(editor.editor.state.zipper); + let* ci = Id.Map.find_opt(index, editor.statics.info_map); + let sketch_seg = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + editor.editor.state.zipper, + ); + ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); + } + ) { + | None => print_endline("prompt generation failed") + | Some(openai_prompt) => + List.iter( + (message: OpenAI.message) => print_endline(message.content), + openai_prompt, + ) + } + ); + Model.{chat: model.chat, currSender: LS} |> Updated.return_quiet; }; }; }; diff --git a/src/haz3lweb/debug/DebugConsole.re b/src/haz3lweb/debug/DebugConsole.re index d85860eaec..5334753d26 100644 --- a/src/haz3lweb/debug/DebugConsole.re +++ b/src/haz3lweb/debug/DebugConsole.re @@ -42,11 +42,15 @@ let print = let* ci = Id.Map.find_opt(index, map); let sketch_seg = Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, zipper); - ChatLSP.Prompt.static_context(ChatLSP.Options.init, ci, sketch_seg); + ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); } ) { | None => print_endline("prompt generation failed") - | Some(prompt) => print_endline(prompt) + | Some(openai_prompt) => + List.iter( + (message: OpenAI.message) => print_endline(message.content), + openai_prompt, + ) } ); | "F10" => diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 8cbeae9055..19437302cd 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -238,9 +238,19 @@ module Update = { {...model, explain_this}; | Assistant(action) => let settings = globals.settings.assistant; + let ed: CellEditor.Model.t = + switch (model.editors) { + | Scratch(m) => List.nth(m.scratchpads, m.current) |> snd + | Documentation(m) => List.nth(m.scratchpads, m.current) |> snd + | Exercises(m) => List.nth(m.exercises, m.current).cells.user_impl // Todo this is an error + }; let* assistant = AssistantModel.Update.update( - ~settings, ~action, ~model=model.assistant, ~schedule_action=a => + ~settings, + ~action, + ~model=model.assistant, + ~editor=ed, + ~schedule_action=a => schedule_action(Assistant(a)) ); {...model, assistant}; From f85d078617b1eb07a6c6691660a9fc4f7fddb79e Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 9 Feb 2025 15:24:32 -0500 Subject: [PATCH 24/50] LS and LLM chats now appear in sidebar --- src/haz3lweb/Settings.re | 4 +- src/haz3lweb/app/assistant/AssistantModel.re | 89 +++++++++---------- .../app/assistant/AssistantSettings.re | 26 ++++++ src/haz3lweb/app/assistant/AssistantView.re | 17 +++- src/haz3lweb/view/Page.re | 2 +- src/util/ListUtil.re | 3 + 6 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 src/haz3lweb/app/assistant/AssistantSettings.re diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index 641cef703c..679e064f2d 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -11,7 +11,7 @@ module Model = { instructor_mode: bool, benchmark: bool, explainThis: ExplainThisModel.Settings.t, - assistant: AssistantModel.Settings.t, + assistant: AssistantSettings.t, sidebar: SidebarModel.Settings.t, }; @@ -103,7 +103,7 @@ module Update = { | Evaluation(evaluation) | Sidebar(SidebarModel.Settings.action) | ExplainThis(ExplainThisModel.Settings.action) - | Assistant(AssistantModel.Settings.action); + | Assistant(AssistantSettings.action); let update = (action, settings: Model.t): Updated.t(Model.t) => { ( diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 0710621d03..b27a549155 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -1,31 +1,9 @@ module Sexp = Sexplib.Sexp; open Haz3lcore; open Util; +open Util.OptUtil.Syntax; -module Settings = { - [@deriving (show({with_path: false}), sexp, yojson)] - type manual_llm = - | Agent - | Human; - - [@deriving (show({with_path: false}), sexp, yojson)] - type manual_lsp = - | LanguageServer - | Human; - - [@deriving (show({with_path: false}), sexp, yojson)] - type t = { - llm: bool, - lsp: bool, - ongoing_chat: bool, - }; - - [@deriving (show({with_path: false}), sexp, yojson)] - type action = - | ToggleLLM - | ToggleLSP - | UpdateChatStatus; -}; +module CodeModel = CodeEditable.Model; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] @@ -67,9 +45,9 @@ module Update = { let update = ( - ~settings: Settings.t, + ~settings: AssistantSettings.t, ~action, - ~editor: CellEditor.Model.t, + ~editor: CodeModel.t, ~model: Model.t, ~schedule_action, ) @@ -105,29 +83,46 @@ module Update = { Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} |> Updated.return_quiet | SendSketch => - Util.OptUtil.Syntax.( - switch ( - { - let* index = Indicated.index(editor.editor.state.zipper); - let* ci = Id.Map.find_opt(index, editor.statics.info_map); - let sketch_seg = - Zipper.smart_seg( - ~dump_backpack=true, - ~erase_buffer=true, - editor.editor.state.zipper, - ); - ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); - } - ) { - | None => print_endline("prompt generation failed") - | Some(openai_prompt) => - List.iter( - (message: OpenAI.message) => print_endline(message.content), + switch ( + { + let* index = Indicated.index(editor.editor.state.zipper); + let* ci = Id.Map.find_opt(index, editor.statics.info_map); + let sketch_seg = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + editor.editor.state.zipper, + ); + ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); + } + ) { + | None => + print_endline("prompt generation failed"); + Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet; + | Some(openai_prompt) => + let messages = + ListUtil.flat_map( + (msg: OpenAI.message): list(string) => {[msg.content]}, openai_prompt, - ) + ); + let prompt = ListUtil.concat_strings(messages); + let llm = OpenAI.Azure_GPT4_0613; + let key = OpenAI.lookup_key(llm); + let params: OpenAI.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenAI.start_chat(~params, ~key, openai_prompt, req => + switch (OpenAI.handle_chat(req)) { + | Some({content, _}) => schedule_action(react(content)) + | None => print_endline("Assistant: response parse failed") + } + ); + let await_llm_response: Model.message = {party: LLM, content: "..."}; + Model.{ + chat: + model.chat @ [{party: LS, content: prompt}, await_llm_response], + currSender: LLM, } - ); - Model.{chat: model.chat, currSender: LS} |> Updated.return_quiet; + |> Updated.return_quiet; + } }; }; }; diff --git a/src/haz3lweb/app/assistant/AssistantSettings.re b/src/haz3lweb/app/assistant/AssistantSettings.re new file mode 100644 index 0000000000..ca744efaa4 --- /dev/null +++ b/src/haz3lweb/app/assistant/AssistantSettings.re @@ -0,0 +1,26 @@ +module Sexp = Sexplib.Sexp; +open Haz3lcore; +open Util; + +[@deriving (show({with_path: false}), sexp, yojson)] +type manual_llm = + | Agent + | Human; + +[@deriving (show({with_path: false}), sexp, yojson)] +type manual_lsp = + | LanguageServer + | Human; + +[@deriving (show({with_path: false}), sexp, yojson)] +type t = { + llm: bool, + lsp: bool, + ongoing_chat: bool, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type action = + | ToggleLLM + | ToggleLSP + | UpdateChatStatus; diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index e05fc811ec..585e4c85c2 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -80,6 +80,19 @@ let resume_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; +let req_button = (~globals: Globals.t, ~inject): Node.t => { + let tooltip = "??"; + let send_sketch = _ => + Virtual_dom.Vdom.Effect.Many([ + inject(AssistantModel.Update.SendSketch), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(send_sketch)], + [Widgets.button_named(~tooltip, None, send_sketch)], + ); +}; + let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { let tooltip = "End Chat"; let end_chat = _ => @@ -256,7 +269,9 @@ let view = [text("Agentic Assistant Chat")], ), globals.settings.assistant.ongoing_chat - ? end_chat_button(~globals, ~inject) : None, + ? req_button(~globals, ~inject) : None, + globals.settings.assistant.ongoing_chat + ? end_chat_button(~globals, ~inject) : None, ], ), globals.settings.assistant.ongoing_chat diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 19437302cd..d858c81141 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -248,8 +248,8 @@ module Update = { AssistantModel.Update.update( ~settings, ~action, + ~editor=ed.editor, ~model=model.assistant, - ~editor=ed, ~schedule_action=a => schedule_action(Assistant(a)) ); diff --git a/src/util/ListUtil.re b/src/util/ListUtil.re index 1e7e87a1af..5f5ebc3990 100644 --- a/src/util/ListUtil.re +++ b/src/util/ListUtil.re @@ -273,6 +273,9 @@ let rec fold_left_map = (final, [y, ...ys]); }; +let concat_strings = (strings: list(string)): string => + List.fold_left((acc, str) => acc ++ str, "", strings); + let rec take_while = (p: 'x => bool, xs: list('x)): (list('x), list('x)) => switch (xs) { | [] => ([], []) From 964f0eadc7b0c8da61417a9e074285602f051a52 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Mon, 10 Feb 2025 12:35:04 -0500 Subject: [PATCH 25/50] prints sketch to sidebar, after LS prompt is printed --- src/haz3lweb/app/assistant/AssistantModel.re | 40 ++++--- src/haz3lweb/app/assistant/AssistantView.re | 109 +++++++++++++++---- src/haz3lweb/debug/DebugConsole.re | 6 +- src/haz3lweb/www/style/assistant.css | 16 ++- src/haz3lweb/www/style/explainthis.css | 2 +- 5 files changed, 132 insertions(+), 41 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index b27a549155..fa23974d38 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -2,6 +2,7 @@ module Sexp = Sexplib.Sexp; open Haz3lcore; open Util; open Util.OptUtil.Syntax; +open Example; module CodeModel = CodeEditable.Model; @@ -16,6 +17,7 @@ module Model = { [@deriving (show({with_path: false}), sexp, yojson)] type message = { party, + code: option(Segment.t), content: string, }; @@ -39,7 +41,7 @@ module Update = { let react = (response: string): t => { // let response = response |> sanitize_response |> quote; - let response: Model.message = {party: LLM, content: response}; + let response: Model.message = {party: LLM, code: None, content: response}; Respond(response); }; @@ -70,7 +72,11 @@ module Update = { } ); }; - let await_llm_response: Model.message = {party: LLM, content: "..."}; + let await_llm_response: Model.message = { + party: LLM, + code: None, + content: "...", + }; Model.{ chat: model.chat @ [message, await_llm_response], currSender: LLM, @@ -83,16 +89,16 @@ module Update = { Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} |> Updated.return_quiet | SendSketch => + let sketch_seg = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + editor.editor.state.zipper, + ); switch ( { let* index = Indicated.index(editor.editor.state.zipper); let* ci = Id.Map.find_opt(index, editor.statics.info_map); - let sketch_seg = - Zipper.smart_seg( - ~dump_backpack=true, - ~erase_buffer=true, - editor.editor.state.zipper, - ); ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); } ) { @@ -101,8 +107,8 @@ module Update = { Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet; | Some(openai_prompt) => let messages = - ListUtil.flat_map( - (msg: OpenAI.message): list(string) => {[msg.content]}, + List.map( + (msg: OpenAI.message): string => {msg.content}, openai_prompt, ); let prompt = ListUtil.concat_strings(messages); @@ -115,14 +121,22 @@ module Update = { | None => print_endline("Assistant: response parse failed") } ); - let await_llm_response: Model.message = {party: LLM, content: "..."}; + let await_llm_response: Model.message = { + party: LLM, + code: None, + content: "...", + }; Model.{ chat: - model.chat @ [{party: LS, content: prompt}, await_llm_response], + model.chat + @ [ + {party: LS, code: Some(sketch_seg), content: prompt}, + await_llm_response, + ], currSender: LLM, } |> Updated.return_quiet; - } + }; }; }; }; diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 585e4c85c2..678cc94123 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -129,6 +129,7 @@ let message_input = let handle_send = (message: string) => { let message: AssistantModel.Model.message = { party: assistantModel.currSender, + code: None, content: message, }; JsUtil.log("Message sent: " ++ message.content); @@ -158,7 +159,7 @@ let message_input = let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); switch (key, ListUtil.last_opt(assistantModel.chat)) { - | (_, Some({party: LLM, content: "..."})) => Virtual_dom.Vdom.Effect.Ignore + | (_, Some({party: LLM, code: None, content: "..."})) => Virtual_dom.Vdom.Effect.Ignore | (Some("Enter"), _) => send_message() | _ => Virtual_dom.Vdom.Effect.Ignore }; @@ -181,7 +182,7 @@ let message_input = (), ), switch (ListUtil.last_opt(assistantModel.chat)) { - | Some({party: LLM, content: "..."}) => + | Some({party: LLM, code: None, content: "..."}) => div( ~attrs=[ clss(["disabled-send-button", "icon"]), @@ -223,27 +224,91 @@ let message_display = ) : Node.t => { let message_nodes = - List.map( - (message: AssistantModel.Model.message) => { - print_endline(message.content); - message.content == "..." && message.party == LLM - ? loading_dots() - : div( - ~attrs=[ - clss([ - "message-container", - message.party == LLM ? "llm" : "ls", - ]), - ], - [ + List.flatten( + List.map( + (message: AssistantModel.Model.message) => { + switch (message.code) { + | Some(sketch) => + message.content == "..." && message.party == LLM + ? [loading_dots()] + : [ div( - ~attrs=[clss(["message-content"])], - [text(message.content)], + ~attrs=[ + clss([ + "message-container", + message.party == LLM ? "llm" : "ls", + ]), + ], + [ + div( + ~attrs=[ + clss([ + message.party == LLM ? "llm-message" : "ls-message", + ]), + ], + [text(message.content)], + ), + ], ), - ], - ); - }, - assistantModel.chat, + div( + ~attrs=[ + clss([ + "message-container", + "example", + message.party == LLM ? "llm" : "ls", + ]), + ], + [ + CellEditor.View.view( + ~globals, + ~signal=_ => Ui_effect.Ignore, + ~inject=_ => Ui_effect.Ignore, + ~selected=None, + ~caption=None, + ~locked=true, + { + sketch + |> Zipper.unzip + |> Editor.Model.mk + |> CellEditor.Model.mk + |> CellEditor.Update.calculate( + ~settings=globals.settings.core, + ~is_edited=true, + ~stitch=x => x, + ~queue_worker=None, + ); + }, + ), + ], + ), + ] + | None => + message.content == "..." && message.party == LLM + ? [loading_dots()] + : [ + div( + ~attrs=[ + clss([ + "message-container", + message.party == LLM ? "llm" : "ls", + ]), + ], + [ + div( + ~attrs=[ + clss([ + message.party == LLM ? "llm-message" : "ls-message", + ]), + ], + [text(message.content)], + ), + ], + ), + ] + } + }, + assistantModel.chat, + ), ); div(~attrs=[clss(["message-display-container"])], message_nodes); }; @@ -271,7 +336,7 @@ let view = globals.settings.assistant.ongoing_chat ? req_button(~globals, ~inject) : None, globals.settings.assistant.ongoing_chat - ? end_chat_button(~globals, ~inject) : None, + ? end_chat_button(~globals, ~inject) : None, ], ), globals.settings.assistant.ongoing_chat diff --git a/src/haz3lweb/debug/DebugConsole.re b/src/haz3lweb/debug/DebugConsole.re index 5334753d26..4002bee841 100644 --- a/src/haz3lweb/debug/DebugConsole.re +++ b/src/haz3lweb/debug/DebugConsole.re @@ -48,7 +48,11 @@ let print = | None => print_endline("prompt generation failed") | Some(openai_prompt) => List.iter( - (message: OpenAI.message) => print_endline(message.content), + (message: OpenAI.message) => { + print_endline("---------- STRING ----------"); + print_endline(message.content); + print_endline("---------- STRING ----------"); + }, openai_prompt, ) } diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 13aacd1c48..9aaf614a46 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -1,7 +1,7 @@ #assistant { display: flex; flex: 1; - width: 20em; + width: 25em; padding: 1em; flex-direction: column; overflow-y: auto; @@ -146,10 +146,18 @@ justify-content: flex-end; } -/* Content of the message */ -#assistant .message-content { +#assistant .llm-message { padding: 8px 12px; - background-color: var(--SAND); + background-color: var(--T1); + border-radius: 12px; + font-size: 1em; + max-width: 80%; + word-wrap: break-word; +} + +#assistant .ls-message { + padding: 8px 12px; + background-color: var(--T3); border-radius: 12px; font-size: 1em; max-width: 80%; diff --git a/src/haz3lweb/www/style/explainthis.css b/src/haz3lweb/www/style/explainthis.css index 78a117c3f1..5787e7923c 100644 --- a/src/haz3lweb/www/style/explainthis.css +++ b/src/haz3lweb/www/style/explainthis.css @@ -1,7 +1,7 @@ #explain-this { display: flex; flex: 1; - width: 20em; + width: 25em; padding: 1em; flex-direction: column; overflow-y: auto; From 052ce2c60517847b0c02f4634236653fc3121a03 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Mon, 10 Feb 2025 19:40:34 -0500 Subject: [PATCH 26/50] adds ability to collapse/expand long messages. This is currently not super in-depth, but you can collapse and expand messages by clicking on them --- src/haz3lweb/app/assistant/AssistantModel.re | 47 ++++++++++++----- src/haz3lweb/app/assistant/AssistantView.re | 55 +++++++++++++++++--- src/haz3lweb/www/style/assistant.css | 17 ++++++ 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index fa23974d38..acacb23176 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -19,6 +19,7 @@ module Model = { party, code: option(Segment.t), content: string, + collapsed: bool, }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -37,14 +38,27 @@ module Update = { | SendMessage(Model.message) | SendSketch | NewChat - | Respond(Model.message); + | Respond(Model.message) + | ToggleCollapse(int); let react = (response: string): t => { // let response = response |> sanitize_response |> quote; - let response: Model.message = {party: LLM, code: None, content: response}; + let response: Model.message = { + party: LLM, + code: None, + content: response, + collapsed: String.length(response) >= 20, + }; Respond(response); }; + let await_llm_response: Model.message = { + party: LLM, + code: None, + content: "...", + collapsed: false, + }; + let update = ( ~settings: AssistantSettings.t, @@ -72,11 +86,6 @@ module Update = { } ); }; - let await_llm_response: Model.message = { - party: LLM, - code: None, - content: "...", - }; Model.{ chat: model.chat @ [message, await_llm_response], currSender: LLM, @@ -121,22 +130,34 @@ module Update = { | None => print_endline("Assistant: response parse failed") } ); - let await_llm_response: Model.message = { - party: LLM, - code: None, - content: "...", - }; Model.{ chat: model.chat @ [ - {party: LS, code: Some(sketch_seg), content: prompt}, + { + party: LS, + code: Some(sketch_seg), + content: prompt, + collapsed: String.length(prompt) >= 20, + }, await_llm_response, ], currSender: LLM, } |> Updated.return_quiet; }; + | ToggleCollapse(index) => + let updated_chat = + List.mapi( + (i: int, msg: Model.message) => + if (i == index) { + {...msg, collapsed: !msg.collapsed}; + } else { + msg; + }, + model.chat, + ); + Model.{...model, chat: updated_chat} |> Updated.return_quiet; }; }; }; diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 678cc94123..7a7aa33d60 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -131,6 +131,7 @@ let message_input = party: assistantModel.currSender, code: None, content: message, + collapsed: String.length(message) >= 20, }; JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ @@ -159,7 +160,7 @@ let message_input = let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); switch (key, ListUtil.last_opt(assistantModel.chat)) { - | (_, Some({party: LLM, code: None, content: "..."})) => Virtual_dom.Vdom.Effect.Ignore + | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore | (Some("Enter"), _) => send_message() | _ => Virtual_dom.Vdom.Effect.Ignore }; @@ -182,7 +183,7 @@ let message_input = (), ), switch (ListUtil.last_opt(assistantModel.chat)) { - | Some({party: LLM, code: None, content: "..."}) => + | Some({party: LLM, code: None, content: "...", collapsed: false}) => div( ~attrs=[ clss(["disabled-send-button", "icon"]), @@ -223,10 +224,18 @@ let message_display = ~assistantModel: AssistantModel.Model.t, ) : Node.t => { + let toggle_collapse = index => { + // Create an action to toggle the collapsed state of a specific message + Virtual_dom.Vdom.Effect.Many([ + inject(AssistantModel.Update.ToggleCollapse(index)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + }; + let message_nodes = List.flatten( - List.map( - (message: AssistantModel.Model.message) => { + List.mapi( + (index: int, message: AssistantModel.Model.message) => { switch (message.code) { | Some(sketch) => message.content == "..." && message.party == LLM @@ -238,6 +247,7 @@ let message_display = "message-container", message.party == LLM ? "llm" : "ls", ]), + Attr.on_click(_ => toggle_collapse(index)), ], [ div( @@ -246,7 +256,23 @@ let message_display = message.party == LLM ? "llm-message" : "ls-message", ]), ], - [text(message.content)], + [ + message.collapsed + ? text( + String.concat( + "", + [ + String.sub( + message.content, + 0, + min(String.length(message.content), 200), + ), + "...", + ], + ), + ) + : text(message.content), + ], ), ], ), @@ -292,6 +318,7 @@ let message_display = "message-container", message.party == LLM ? "llm" : "ls", ]), + Attr.on_click(_ => toggle_collapse(index)), ], [ div( @@ -300,7 +327,23 @@ let message_display = message.party == LLM ? "llm-message" : "ls-message", ]), ], - [text(message.content)], + [ + message.collapsed + ? text( + String.concat( + "", + [ + String.sub( + message.content, + 0, + min(String.length(message.content), 200), + ), + "...", + ], + ), + ) + : text(message.content), + ], ), ], ), diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 9aaf614a46..9ad5884ba6 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -138,6 +138,23 @@ margin-bottom: 10px; } +#assistant .collapse-indicator { + width: 100%; + text-align: center; + color: var(--STONE); + font-size: 0.8em; + opacity: 0.7; + margin-top: 4px; +} + +#assistant .message-container.collapsed .collapse-indicator::after { + content: "▼ Show more"; +} + +#assistant .message-container.expanded .collapse-indicator::after { + content: "▲ Show less"; +} + #assistant .llm { justify-content: none; } From e1700d5c94392fe131a2f0c78f8fc9f9eaa7b402 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Mon, 10 Feb 2025 19:42:21 -0500 Subject: [PATCH 27/50] fixed up some things in that last push --- src/haz3lweb/app/assistant/AssistantModel.re | 4 ++-- src/haz3lweb/app/assistant/AssistantView.re | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index acacb23176..d03573e5ae 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -47,7 +47,7 @@ module Update = { party: LLM, code: None, content: response, - collapsed: String.length(response) >= 20, + collapsed: String.length(response) >= 200, }; Respond(response); }; @@ -138,7 +138,7 @@ module Update = { party: LS, code: Some(sketch_seg), content: prompt, - collapsed: String.length(prompt) >= 20, + collapsed: String.length(prompt) >= 200, }, await_llm_response, ], diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 7a7aa33d60..83bf394395 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -131,7 +131,7 @@ let message_input = party: assistantModel.currSender, code: None, content: message, - collapsed: String.length(message) >= 20, + collapsed: String.length(message) >= 200, }; JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ @@ -258,6 +258,7 @@ let message_display = ], [ message.collapsed + && String.length(message.content) >= 200 ? text( String.concat( "", From a771374b53ba5c840353d67a70746ab5fca02172 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 11:33:40 -0500 Subject: [PATCH 28/50] =?UTF-8?q?=F0=9F=94=91=F0=9F=A6=89=20adds=20interfa?= =?UTF-8?q?ce=20and=20local=20storage=20updating=20allowing=20use=20to=20e?= =?UTF-8?q?nter=20their=20own=20api=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/haz3lweb/Store.re | 1 - src/haz3lweb/app/assistant/AssistantModel.re | 30 ++- src/haz3lweb/app/assistant/AssistantView.re | 92 ++++++++- src/haz3lweb/util/API/OpenRouter.re | 207 +++++++++++++++++++ src/haz3lweb/view/Page.re | 3 + src/haz3lweb/www/style/assistant.css | 98 ++++++++- 6 files changed, 417 insertions(+), 14 deletions(-) create mode 100644 src/haz3lweb/util/API/OpenRouter.re diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index 64de4bb29b..8827dd66e9 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -72,7 +72,6 @@ module F = }; }; -// todo russ: make work with .F module Generic = { let prefix: string = "KEY_STORE_"; diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index d03573e5ae..63c9e40b65 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -36,6 +36,7 @@ module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = | SendMessage(Model.message) + | SetKey(string) | SendSketch | NewChat | Respond(Model.message) @@ -43,13 +44,27 @@ module Update = { let react = (response: string): t => { // let response = response |> sanitize_response |> quote; - let response: Model.message = { - party: LLM, - code: None, - content: response, - collapsed: String.length(response) >= 200, + let zipper_of_response = Printer.zipper_of_string(response); + switch (zipper_of_response) { + | Some(z) => + let segment_of_response = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); + let response_as_message: Model.message = { + party: LLM, + code: Some(segment_of_response), + content: "", + collapsed: String.length(response) >= 200, + }; + Respond(response_as_message); + | None => + let response_as_message: Model.message = { + party: LLM, + code: None, + content: response, + collapsed: String.length(response) >= 200, + }; + Respond(response_as_message); }; - Respond(response); }; let await_llm_response: Model.message = { @@ -93,6 +108,9 @@ module Update = { |> Updated.return_quiet; | _ => Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet } + | SetKey(api_key) => + Store.Generic.save("API", api_key); + model |> Updated.return_quiet; | NewChat => Model.{chat: [], currSender: LS} |> Updated.return_quiet | Respond(message) => Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 83bf394395..1af2a5ec5b 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -2,6 +2,7 @@ open Virtual_dom.Vdom; open Node; open Util.Web; open Util; +open Util.OptUtil.Syntax; open Haz3lcore; open Js_of_ocaml; @@ -110,14 +111,92 @@ let settings_box = (~globals: Globals.t, ~inject): Node.t => { div( ~attrs=[clss(["settings-box"])], [ - llm_toggle(~globals), - lsp_toggle(~globals), + // llm_toggle(~globals), + // lsp_toggle(~globals), begin_chat_button(~globals, ~inject), resume_chat_button(~globals, ~inject), ], ); }; +let api_input = + ( + ~signal, + ~inject, + ~globals: Globals.t, + ~assistantModel: AssistantModel.Model.t, + ) + : Node.t => { + let handle_submission = (api_key: string) => { + JsUtil.log("Your API key for this session has been set: " ++ api_key); + Virtual_dom.Vdom.Effect.Many([ + inject(AssistantModel.Update.SetKey(api_key)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + }; + let submit_key = _ => { + let message = + Js.Opt.case( + Dom_html.document##getElementById(Js.string("api-input")), + () => "", + el => + switch (Js.Unsafe.coerce(el)) { + | input => Js.to_string(input##.value) + }, + ); + Js.Opt.case( + Dom_html.document##getElementById(Js.string("api-input")), + () => (), + el => Js.Unsafe.coerce(el)##.value := Js.string(""), + ); + handle_submission(message); + }; + let handle_keydown = event => { + let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); + switch (key, ListUtil.last_opt(assistantModel.chat)) { + | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore + | (Some("Enter"), _) => submit_key() + | _ => Virtual_dom.Vdom.Effect.Ignore + }; + }; + + div( + ~attrs=[clss(["api-key-container"])], + [ + input( + ~attrs=[ + Attr.id("api-input"), + Attr.placeholder("Enter your API key..."), + Attr.type_("text"), + Attr.property("autocomplete", Js.Unsafe.inject("off")), + Attr.on_focus(_ => + signal(MakeActive(ScratchMode.Selection.TextBox)) + ), + Attr.on_keydown(handle_keydown), + clss(["api-input"]), + ], + (), + ), + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(submit_key)], + [Widgets.button_named(~tooltip="Update API Key", None, submit_key)], + ), + div(~attrs=[clss(["text-display"])], [text("Current API Key:\n")]), + div( + ~attrs=[clss(["api-key-display"])], + [ + text( + Option.value( + Store.Generic.load("API"), + ~default="No API key set", + ), + ), + ], + ), + ], + ); +}; + let message_input = ( ~signal, @@ -186,7 +265,7 @@ let message_input = | Some({party: LLM, code: None, content: "...", collapsed: false}) => div( ~attrs=[ - clss(["disabled-send-button", "icon"]), + clss(["send-button-disabled", "icon"]), Attr.title("Submitting Message Disabled"), ], [Icons.thin_x], @@ -330,6 +409,7 @@ let message_display = ], [ message.collapsed + && String.length(message.content) >= 200 ? text( String.concat( "", @@ -383,14 +463,16 @@ let view = ? end_chat_button(~globals, ~inject) : None, ], ), - globals.settings.assistant.ongoing_chat - ? None : settings_box(~globals, ~inject), globals.settings.assistant.ongoing_chat ? message_display(~signal, ~inject, ~globals, ~assistantModel) : None, globals.settings.assistant.ongoing_chat ? message_input(~signal, ~inject, ~globals, ~assistantModel) : None, + globals.settings.assistant.ongoing_chat + ? None : api_input(~signal, ~inject, ~globals, ~assistantModel), + globals.settings.assistant.ongoing_chat + ? None : settings_box(~globals, ~inject), ], ), ], diff --git a/src/haz3lweb/util/API/OpenRouter.re b/src/haz3lweb/util/API/OpenRouter.re new file mode 100644 index 0000000000..20317699c7 --- /dev/null +++ b/src/haz3lweb/util/API/OpenRouter.re @@ -0,0 +1,207 @@ +module Sexp = Sexplib.Sexp; +open API; +open Util.OptUtil.Syntax; +open Util; + +[@deriving (show({with_path: false}), sexp, yojson)] +type chat_models = + | GPT4 + | GPT3_5Turbo + | Azure_GPT4_0613 + | Azure_GPT3_5Turbo; + +[@deriving (show({with_path: false}), sexp, yojson)] +type role = + | System + | User + | Assistant + | Function; + +[@deriving (show({with_path: false}), sexp, yojson)] +type params = { + llm: chat_models, + temperature: float, + top_p: float, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type message = { + role, + content: string, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type prompt = list(message); + +[@deriving (show({with_path: false}), sexp, yojson)] +type usage = { + prompt_tokens: int, + completion_tokens: int, + total_tokens: int, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type reply = { + content: string, + usage, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +let string_of_chat_model = + fun + | GPT4 => "gpt-4" + | GPT3_5Turbo => "gpt-3.5-turbo" + | Azure_GPT4_0613 => "azure-gpt-4" + | Azure_GPT3_5Turbo => "azure-gpt-3.5-turbo"; + +let string_of_role = + fun + | System => "system" + | User => "user" + | Assistant => "assistant" + | Function => "function"; + +let default_params = {llm: Azure_GPT4_0613, temperature: 1.0, top_p: 1.0}; + +let mk_message = ({role, content}) => + `Assoc([ + ("role", `String(string_of_role(role))), + ("content", `String(content)), + ]); + +let body = (~params: params, messages: prompt): Json.t => { + `Assoc([ + ("model", `String(string_of_chat_model(params.llm))), + ("temperature", `Float(params.temperature)), + ("top_p", `Float(params.top_p)), + ("messages", `List(List.map(mk_message, messages))), + ]); +}; + +let lookup_key = (llm: chat_models) => + switch (llm) { + | Azure_GPT3_5Turbo => Store.Generic.load("AZURE") + | Azure_GPT4_0613 => Store.Generic.load("AZURE4") + | GPT3_5Turbo + | GPT4 => Store.Generic.load("OpenAI") + }; + +/* SAMPLE OPENAI CHAT RESPONSE: + { + "id":"chatcmpl-6y5167eYM6ovo5yVThXzr5CB8oVIO", + "object":"chat.completion", + "created":1679776984, + "model":"gpt-3.5-turbo-0301", + "usage":{ + "prompt_tokens":25, + "completion_tokens":1, + "total_tokens":26 + }, + "choices":[ + { + "message":{ + "role":"assistant", + "content":"576" + }, + "finish_reason":"stop", + "index":0 + } + ] + }*/ + +let chat = (~key, ~body, ~handler): unit => + switch (key) { + | None => print_endline("API: OpenAI KEY NOT FOUND") + | Some(api_key) => + print_endline("API: POSTing OpenAI request"); + request( + ~method=POST, + ~url="https://api.openai.com/v1/chat/completions", + ~headers=[ + ("Content-Type", "application/json"), + ("Authorization", "Bearer " ++ api_key), + ], + ~body, + handler, + ); + }; + +let azure_request = + (~key, ~resource, ~deployment, ~api_version, ~body, ~handler): unit => + switch (key) { + | None => print_endline("API: KEY NOT FOUND") + | Some(api_key) => + print_endline("API: POSTing Azure request"); + request( + ~method=POST, + ~url= + Printf.sprintf( + "https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", + resource, + deployment, + api_version, + ), + ~headers=[("Content-Type", "application/json"), ("api-key", api_key)], + ~body, + handler, + ); + }; + +let chat_azure35 = + azure_request( + ~resource="hazel", + ~deployment="gpt35turbo", + ~api_version="2023-05-15", + ); + +let chat_azure4 = + azure_request( + ~resource="hazel2", + ~deployment="hazel-gpt-4", + ~api_version="2023-05-15", + ); + +let start_chat = (~params, ~key, prompt: prompt, handler): unit => { + let body = body(~params, prompt); + switch (params.llm) { + | Azure_GPT3_5Turbo => chat_azure35(~key, ~body, ~handler) + | Azure_GPT4_0613 => chat_azure4(~key, ~body, ~handler) + | GPT3_5Turbo + | GPT4 => chat(~key, ~body, ~handler) + }; +}; + +let int_field = (json: Json.t, field: string) => { + let* num = Json.dot(field, json); + Json.int(num); +}; + +let of_usage = (choices: Json.t): option(usage) => { + let* prompt_tokens = int_field(choices, "prompt_tokens"); + let* completion_tokens = int_field(choices, "completion_tokens"); + let+ total_tokens = int_field(choices, "total_tokens"); + {prompt_tokens, completion_tokens, total_tokens}; +}; + +let first_message_content = (choices: Json.t): option(string) => { + let* choices = Json.list(choices); + let* hd = Util.ListUtil.hd_opt(choices); + let* message = Json.dot("message", hd); + let* content = Json.dot("content", message); + Json.str(content); +}; + +let handle_chat = (~db=ignore, response: option(Json.t)): option(reply) => { + db("OpenAI: Chat response:"); + Option.map(r => r |> Json.to_string |> db, response) |> ignore; + let* json = response; + let* choices = Json.dot("choices", json); + let* usage = Json.dot("usage", json); + let* content = first_message_content(choices); + let+ usage = of_usage(usage); + {content, usage}; +}; + +let add_to_prompt = (prompt, ~assistant, ~user): prompt => + prompt + @ [{role: Assistant, content: assistant}, {role: User, content: user}]; diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index d858c81141..133f16c28e 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -364,6 +364,7 @@ module View = { let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); switch (elId) { | Some("message-input") => () + | Some("api-input") => () | _ => JsUtil.copy( (cursor.selected_text |> Option.value(~default=() => ""))(), @@ -380,6 +381,7 @@ module View = { let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); switch (elId) { | Some("message-input") => Effect.Ignore + | Some("api-input") => Effect.Ignore | _ => JsUtil.copy( (cursor.selected_text |> Option.value(~default=() => ""))(), @@ -414,6 +416,7 @@ module View = { let elId = Js.Opt.to_option(Js.Unsafe.coerce(el)##.id); switch (elId) { | Some("message-input") => Effect.Ignore + | Some("api-input") => Effect.Ignore | _ => let pasted_text = Js.to_string(evt##.clipboardData##getData(Js.string("text"))) diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 9ad5884ba6..e3c240c525 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -36,7 +36,7 @@ flex-direction: column; align-items: center; gap: 2.25em; - margin-top: 1em; + margin-top: auto; } #assistant .chat-button { @@ -212,4 +212,98 @@ transform: scale(1); opacity: 1; } - } \ No newline at end of file + } + + /* API key container styling */ +#assistant .api-key-container { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + align-items: center; + margin-top: 1em; +} + +#assistant .api-input { + width: 85%; + background-color: var(--SAND); + padding: 14px; + outline: none; + border-radius: 3.5px; + border: none; + transition: all 0.3s ease; + margin-bottom: 10px; +} + +#assistant .api-input:focus { + box-shadow: 0px 0px 1px var(--SHADOW), 0 0 0 1px var(--SHADOW); +} + +/* LLM and LSP toggle button containers */ +#assistant .llm-button, +#assistant .lsp-button { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 0.5em; + padding: 0 1em; +} + +/* Example message container */ +#assistant .message-container.example { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} + +#assistant .message-container.example .cell-editor { + width: 100%; + background-color: var(--UI-Background); + border-radius: 8px; + padding: 8px; +} + +#assistant .api-key-display { + width: 85%; + padding: 12px; + background-color: var(--UI-Background); + border-radius: 3.5px; + border: 1px solid var(--SAND); + font-size: 0.9em; + color: var(--STONE); + text-align: center; + word-wrap: break-word; + margin-bottom: 10px; + font-family: monospace; + overflow-x: auto; +} + +#assistant .api-key-label { + font-size: 0.85em; + color: var(--STONE); + opacity: 0.7; + margin-bottom: 5px; +} + +#assistant .api-key-container { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + align-items: center; + margin-top: 1em; + margin-bottom: 1em; +} + +#assistant .text-display { + width: 85%; + padding: 12px; + background-color: var(--UI-Background); + font-size: 0.8em; + color: var(--STONE); + /* text-align: center; */ + word-wrap: break-word; + margin-bottom: -15px; + overflow-x: auto; +} \ No newline at end of file From ae7a851eefe4183139723981497a74a3062a7f17 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 12:27:08 -0500 Subject: [PATCH 29/50] begins integration of OpenRouter --- src/haz3lweb/app/assistant/AssistantModel.re | 26 +++--- src/haz3lweb/app/assistant/ChatLSP.re | 14 +-- src/haz3lweb/app/assistant/Oracle.re | 7 +- src/haz3lweb/debug/DebugConsole.re | 6 +- src/haz3lweb/util/API/OpenRouter.re | 96 ++++---------------- 5 files changed, 44 insertions(+), 105 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 63c9e40b65..552d1037d6 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -91,11 +91,11 @@ module Update = { switch (Oracle.ask(message.content)) { | None => print_endline("Oracle: prompt generation failed") | Some(prompt) => - let llm = OpenAI.Azure_GPT4_0613; - let key = OpenAI.lookup_key(llm); - let params: OpenAI.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenAI.start_chat(~params, ~key, prompt, req => - switch (OpenAI.handle_chat(req)) { + let llm = OpenRouter.Gemini_Flash_Lite; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, prompt, req => + switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => schedule_action(react(content)) | None => print_endline("Assistant: response parse failed") } @@ -132,18 +132,18 @@ module Update = { | None => print_endline("prompt generation failed"); Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet; - | Some(openai_prompt) => + | Some(openrouter_prompt) => let messages = List.map( - (msg: OpenAI.message): string => {msg.content}, - openai_prompt, + (msg: OpenRouter.message): string => {msg.content}, + openrouter_prompt, ); let prompt = ListUtil.concat_strings(messages); - let llm = OpenAI.Azure_GPT4_0613; - let key = OpenAI.lookup_key(llm); - let params: OpenAI.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenAI.start_chat(~params, ~key, openai_prompt, req => - switch (OpenAI.handle_chat(req)) { + let llm = OpenRouter.Gemini_Flash_Lite; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => + switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => schedule_action(react(content)) | None => print_endline("Assistant: response parse failed") } diff --git a/src/haz3lweb/app/assistant/ChatLSP.re b/src/haz3lweb/app/assistant/ChatLSP.re index cee933056c..2e3c9cd6ce 100644 --- a/src/haz3lweb/app/assistant/ChatLSP.re +++ b/src/haz3lweb/app/assistant/ChatLSP.re @@ -16,7 +16,7 @@ let statics_of_exp_zipper = module Options = { [@deriving (show({with_path: false}), sexp, yojson)] type t = { - params: OpenAI.params, + params: OpenRouter.params, instructions: bool, syntax_notes: bool, num_examples: int, @@ -26,7 +26,7 @@ module Options = { }; let init: t = { - params: OpenAI.default_params, + params: OpenRouter.default_params, instructions: true, syntax_notes: true, num_examples: 9, @@ -798,9 +798,9 @@ module Prompt = { mk_user_message(sketch, ~expected_ty, ~relevant_ctx); }; - let samples = (num_examples: int): list(OpenAI.message) => + let samples = (num_examples: int): list(OpenRouter.message) => Util.ListUtil.flat_map( - ((sketch, expected_ty, completion)): list(OpenAI.message) => + ((sketch, expected_ty, completion)): list(OpenRouter.message) => [ { role: User, @@ -814,14 +814,14 @@ module Prompt = { let mk_init = (options: Options.t, ci: Info.t, sketch: Segment.t) - : option(OpenAI.prompt) => { + : option(OpenRouter.prompt) => { let+ user_message = static_context(options, ci, sketch); - OpenAI.[{role: System, content: SystemPrompt.mk(options)}] + OpenRouter.[{role: System, content: SystemPrompt.mk(options)}] @ samples(options.num_examples) @ [{role: User, content: user_message}]; }; - let mk_error = (ci: Info.t, reply: OpenAI.reply): option(string) => { + let mk_error = (ci: Info.t, reply: OpenRouter.reply): option(string) => { /* TODO: This should maybe take whole JSON convo * so far and return an appended version */ //TODO: Proper errors diff --git a/src/haz3lweb/app/assistant/Oracle.re b/src/haz3lweb/app/assistant/Oracle.re index f30bb013ce..9e04ee8f66 100644 --- a/src/haz3lweb/app/assistant/Oracle.re +++ b/src/haz3lweb/app/assistant/Oracle.re @@ -12,7 +12,7 @@ let sanitize_prompt = (prompt: string): string => { prompt; }; -let ask = (body: string): option(OpenAI.prompt) => { +let ask = (body: string): option(OpenRouter.prompt) => { /* let system_prompt = [ "Respond as minimally as possible", @@ -22,10 +22,7 @@ let ask = (body: string): option(OpenAI.prompt) => { switch (String.trim(body)) { | "" => None | _ => - let input = - /* [OpenAI.{role: System, content: String.concat("\n", system_prompt)}] - @ */ - [{OpenAI.role: User, OpenAI.content: body}]; + let input = [{OpenRouter.role: User, OpenRouter.content: body}]; Some(input); }; }; diff --git a/src/haz3lweb/debug/DebugConsole.re b/src/haz3lweb/debug/DebugConsole.re index 4002bee841..7c24936fbc 100644 --- a/src/haz3lweb/debug/DebugConsole.re +++ b/src/haz3lweb/debug/DebugConsole.re @@ -46,14 +46,14 @@ let print = } ) { | None => print_endline("prompt generation failed") - | Some(openai_prompt) => + | Some(prompt) => List.iter( - (message: OpenAI.message) => { + (message: OpenRouter.message) => { print_endline("---------- STRING ----------"); print_endline(message.content); print_endline("---------- STRING ----------"); }, - openai_prompt, + prompt, ) } ); diff --git a/src/haz3lweb/util/API/OpenRouter.re b/src/haz3lweb/util/API/OpenRouter.re index 20317699c7..9530c82da7 100644 --- a/src/haz3lweb/util/API/OpenRouter.re +++ b/src/haz3lweb/util/API/OpenRouter.re @@ -5,10 +5,10 @@ open Util; [@deriving (show({with_path: false}), sexp, yojson)] type chat_models = - | GPT4 - | GPT3_5Turbo - | Azure_GPT4_0613 - | Azure_GPT3_5Turbo; + | DeepSeek_V3 + | O3_Mini_High + | Gemini_Experimental_1206 + | Gemini_Flash_Lite; [@deriving (show({with_path: false}), sexp, yojson)] type role = @@ -49,10 +49,10 @@ type reply = { [@deriving (show({with_path: false}), sexp, yojson)] let string_of_chat_model = fun - | GPT4 => "gpt-4" - | GPT3_5Turbo => "gpt-3.5-turbo" - | Azure_GPT4_0613 => "azure-gpt-4" - | Azure_GPT3_5Turbo => "azure-gpt-3.5-turbo"; + | DeepSeek_V3 => "deepseek-v3" + | O3_Mini_High => "o3-mini-high" + | Gemini_Experimental_1206 => "gemini-experimental-1206" + | Gemini_Flash_Lite => "google/gemini-2.0-flash-lite-preview-02-05:free"; let string_of_role = fun @@ -61,7 +61,7 @@ let string_of_role = | Assistant => "assistant" | Function => "function"; -let default_params = {llm: Azure_GPT4_0613, temperature: 1.0, top_p: 1.0}; +let default_params = {llm: Gemini_Flash_Lite, temperature: 1.0, top_p: 1.0}; let mk_message = ({role, content}) => `Assoc([ @@ -80,43 +80,20 @@ let body = (~params: params, messages: prompt): Json.t => { let lookup_key = (llm: chat_models) => switch (llm) { - | Azure_GPT3_5Turbo => Store.Generic.load("AZURE") - | Azure_GPT4_0613 => Store.Generic.load("AZURE4") - | GPT3_5Turbo - | GPT4 => Store.Generic.load("OpenAI") + | DeepSeek_V3 => Store.Generic.load("API") + | O3_Mini_High => Store.Generic.load("API") + | Gemini_Experimental_1206 => Store.Generic.load("API") + | Gemini_Flash_Lite => Store.Generic.load("API") // Adjust if using a different key }; -/* SAMPLE OPENAI CHAT RESPONSE: - { - "id":"chatcmpl-6y5167eYM6ovo5yVThXzr5CB8oVIO", - "object":"chat.completion", - "created":1679776984, - "model":"gpt-3.5-turbo-0301", - "usage":{ - "prompt_tokens":25, - "completion_tokens":1, - "total_tokens":26 - }, - "choices":[ - { - "message":{ - "role":"assistant", - "content":"576" - }, - "finish_reason":"stop", - "index":0 - } - ] - }*/ - let chat = (~key, ~body, ~handler): unit => switch (key) { | None => print_endline("API: OpenAI KEY NOT FOUND") | Some(api_key) => - print_endline("API: POSTing OpenAI request"); + print_endline("API: POSTing OpenRouter request"); request( ~method=POST, - ~url="https://api.openai.com/v1/chat/completions", + ~url="https://openrouter.ai/api/v1/chat/completions", ~headers=[ ("Content-Type", "application/json"), ("Authorization", "Bearer " ++ api_key), @@ -126,48 +103,13 @@ let chat = (~key, ~body, ~handler): unit => ); }; -let azure_request = - (~key, ~resource, ~deployment, ~api_version, ~body, ~handler): unit => - switch (key) { - | None => print_endline("API: KEY NOT FOUND") - | Some(api_key) => - print_endline("API: POSTing Azure request"); - request( - ~method=POST, - ~url= - Printf.sprintf( - "https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", - resource, - deployment, - api_version, - ), - ~headers=[("Content-Type", "application/json"), ("api-key", api_key)], - ~body, - handler, - ); - }; - -let chat_azure35 = - azure_request( - ~resource="hazel", - ~deployment="gpt35turbo", - ~api_version="2023-05-15", - ); - -let chat_azure4 = - azure_request( - ~resource="hazel2", - ~deployment="hazel-gpt-4", - ~api_version="2023-05-15", - ); - let start_chat = (~params, ~key, prompt: prompt, handler): unit => { let body = body(~params, prompt); switch (params.llm) { - | Azure_GPT3_5Turbo => chat_azure35(~key, ~body, ~handler) - | Azure_GPT4_0613 => chat_azure4(~key, ~body, ~handler) - | GPT3_5Turbo - | GPT4 => chat(~key, ~body, ~handler) + | DeepSeek_V3 => chat(~key, ~body, ~handler) + | O3_Mini_High => chat(~key, ~body, ~handler) + | Gemini_Experimental_1206 => chat(~key, ~body, ~handler) + | Gemini_Flash_Lite => chat(~key, ~body, ~handler) // Add Dolphin handling }; }; From dbc63c3894f3c4af4a6bc1bb9348d6f1a4d11d7d Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 13:06:57 -0500 Subject: [PATCH 30/50] adds a prompt for the LLM, giving it context to the entire conversation --- src/haz3lweb/app/assistant/AssistantModel.re | 75 +++++++++++++------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 552d1037d6..0a46832733 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -42,29 +42,30 @@ module Update = { | Respond(Model.message) | ToggleCollapse(int); - let react = (response: string): t => { + let react = (~response: string, ~code_suggestion: bool): t => { // let response = response |> sanitize_response |> quote; let zipper_of_response = Printer.zipper_of_string(response); - switch (zipper_of_response) { - | Some(z) => - let segment_of_response = - Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); - let response_as_message: Model.message = { - party: LLM, - code: Some(segment_of_response), - content: "", - collapsed: String.length(response) >= 200, - }; - Respond(response_as_message); - | None => - let response_as_message: Model.message = { - party: LLM, - code: None, - content: response, - collapsed: String.length(response) >= 200, - }; - Respond(response_as_message); + let response_as_message: Model.message = { + party: LLM, + code: None, + content: response, + collapsed: String.length(response) >= 200, }; + code_suggestion + ? switch (zipper_of_response) { + | Some(z) => + let segment_of_response = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); + let response_as_message: Model.message = { + party: LLM, + code: Some(segment_of_response), + content: "", + collapsed: String.length(response) >= 200, + }; + Respond(response_as_message); + | None => Respond(response_as_message) + } + : Respond(response_as_message); }; let await_llm_response: Model.message = { @@ -74,6 +75,27 @@ module Update = { collapsed: false, }; + let collect_chat = (~messages: list(Model.message)): string => { + let chat = "The following is a log of the current conversation. This is solely for the purpose + to help you recall the entire conversation, in case the user asks you something that needs context + from before. You should respond as normal, using the entire chat as context, and understand that the + most recent \"User Input\" is what the user is currently sending/asking, and is what your main focus should be. + For the most part, you should treat this solely as a prompt, and not explicitly acknowledge it in your + reponse. Here is the conversation for context: "; + List.fold_left( + (chat: string, message: Model.message) => + if (message.party == LLM) { + chat ++ "Your Reponse: " ++ message.content ++ " "; + } else if (message.party == LS) { + chat ++ "User Input: " ++ message.content ++ " "; + } else { + chat ++ message.content; + }, + chat, + messages, + ); + }; + let update = ( ~settings: AssistantSettings.t, @@ -85,10 +107,11 @@ module Update = { : Updated.t(Model.t) => { switch (action) { | SendMessage(message) => - // todo: send API Call here switch (message.party) { | LS => - switch (Oracle.ask(message.content)) { + let collected_chat = collect_chat(~messages=model.chat @ [message]); + print_endline(collected_chat); + switch (Oracle.ask(collected_chat)) { | None => print_endline("Oracle: prompt generation failed") | Some(prompt) => let llm = OpenRouter.Gemini_Flash_Lite; @@ -96,7 +119,10 @@ module Update = { let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; OpenRouter.start_chat(~params, ~key, prompt, req => switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => schedule_action(react(content)) + | Some({content, _}) => + schedule_action( + react(~response=content, ~code_suggestion=false), + ) | None => print_endline("Assistant: response parse failed") } ); @@ -144,7 +170,8 @@ module Update = { let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => schedule_action(react(content)) + | Some({content, _}) => + schedule_action(react(~response=content, ~code_suggestion=true)) | None => print_endline("Assistant: response parse failed") } ); From fb2d4f980e6f698439ef5294d7f9afe4729526d1 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 13:41:59 -0500 Subject: [PATCH 31/50] allows use to select from different models; currently has a gemini and a llama model --- src/haz3lweb/app/assistant/AssistantModel.re | 29 ++++++++---- src/haz3lweb/app/assistant/AssistantView.re | 47 ++++++++++++++++++++ src/haz3lweb/util/API/OpenRouter.re | 24 ++++------ src/haz3lweb/www/style/assistant.css | 26 +++++++++++ 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index 0a46832733..ff4e513810 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -26,10 +26,11 @@ module Model = { type t = { chat: list(message) /*To-do: Add chat ids for saving past chats*/, currSender: party, + llm: OpenRouter.chat_models, }; [@deriving (show({with_path: false}), sexp, yojson)] - let init: t = {chat: [], currSender: LS}; + let init: t = {chat: [], currSender: LS, llm: Gemini_Flash_Lite}; }; module Update = { @@ -40,7 +41,8 @@ module Update = { | SendSketch | NewChat | Respond(Model.message) - | ToggleCollapse(int); + | ToggleCollapse(int) + | SelectLLM(OpenRouter.chat_models); let react = (~response: string, ~code_suggestion: bool): t => { // let response = response |> sanitize_response |> quote; @@ -114,7 +116,7 @@ module Update = { switch (Oracle.ask(collected_chat)) { | None => print_endline("Oracle: prompt generation failed") | Some(prompt) => - let llm = OpenRouter.Gemini_Flash_Lite; + let llm = model.llm; let key = Store.Generic.load("API"); let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; OpenRouter.start_chat(~params, ~key, prompt, req => @@ -128,18 +130,26 @@ module Update = { ); }; Model.{ + ...model, chat: model.chat @ [message, await_llm_response], currSender: LLM, } |> Updated.return_quiet; - | _ => Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet + | _ => + Model.{...model, chat: model.chat, currSender: LLM} + |> Updated.return_quiet } | SetKey(api_key) => Store.Generic.save("API", api_key); model |> Updated.return_quiet; - | NewChat => Model.{chat: [], currSender: LS} |> Updated.return_quiet + | NewChat => + Model.{...model, chat: [], currSender: LS} |> Updated.return_quiet | Respond(message) => - Model.{chat: ListUtil.leading(model.chat) @ [message], currSender: LS} + Model.{ + ...model, + chat: ListUtil.leading(model.chat) @ [message], + currSender: LS, + } |> Updated.return_quiet | SendSketch => let sketch_seg = @@ -157,7 +167,8 @@ module Update = { ) { | None => print_endline("prompt generation failed"); - Model.{chat: model.chat, currSender: LLM} |> Updated.return_quiet; + Model.{...model, chat: model.chat, currSender: LLM} + |> Updated.return_quiet; | Some(openrouter_prompt) => let messages = List.map( @@ -165,7 +176,7 @@ module Update = { openrouter_prompt, ); let prompt = ListUtil.concat_strings(messages); - let llm = OpenRouter.Gemini_Flash_Lite; + let llm = model.llm; let key = Store.Generic.load("API"); let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => @@ -176,6 +187,7 @@ module Update = { } ); Model.{ + ...model, chat: model.chat @ [ @@ -203,6 +215,7 @@ module Update = { model.chat, ); Model.{...model, chat: updated_chat} |> Updated.return_quiet; + | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet }; }; }; diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index 1af2a5ec5b..a512df27e7 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -1,9 +1,11 @@ +module Sexp = Sexplib.Sexp; open Virtual_dom.Vdom; open Node; open Util.Web; open Util; open Util.OptUtil.Syntax; open Haz3lcore; + open Js_of_ocaml; type selection = @@ -107,6 +109,49 @@ let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; +let select_llm = + ( + ~signal, + ~inject, + ~globals: Globals.t, + ~assistantModel: AssistantModel.Model.t, + ) + : Node.t => { + let handle_change = (event, _) => { + let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); + let selected_llm = + switch (value) { + | "Gemini Flash Lite 2.0" => OpenRouter.Gemini_Flash_Lite + | "Llama 3.1 Nemotron 70B" => OpenRouter.Llama_3_1_Nemo + | _ => OpenRouter.Gemini_Flash_Lite + }; + Virtual_dom.Vdom.Effect.Many([ + inject(AssistantModel.Update.SelectLLM(selected_llm)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + }; + + div( + ~attrs=[clss(["llm-selector"])], + [ + label(~attrs=[clss(["llm-label"])], [text("Select LLM Model: ")]), + select( + ~attrs=[Attr.on_change(handle_change), clss(["llm-dropdown"])], + [ + option( + ~attrs=[Attr.value("Gemini_Flash_Lite")], + [text("Gemini Flash Lite 2.0")], + ), + option( + ~attrs=[Attr.value("Llama 3.1 Nemotron 70B")], + [text("Llama 3.1 Nemotron 70B")], + ), + ], + ), + ], + ); +}; + let settings_box = (~globals: Globals.t, ~inject): Node.t => { div( ~attrs=[clss(["settings-box"])], @@ -471,6 +516,8 @@ let view = : None, globals.settings.assistant.ongoing_chat ? None : api_input(~signal, ~inject, ~globals, ~assistantModel), + globals.settings.assistant.ongoing_chat + ? None : select_llm(~signal, ~inject, ~globals, ~assistantModel), globals.settings.assistant.ongoing_chat ? None : settings_box(~globals, ~inject), ], diff --git a/src/haz3lweb/util/API/OpenRouter.re b/src/haz3lweb/util/API/OpenRouter.re index 9530c82da7..d907418361 100644 --- a/src/haz3lweb/util/API/OpenRouter.re +++ b/src/haz3lweb/util/API/OpenRouter.re @@ -5,10 +5,8 @@ open Util; [@deriving (show({with_path: false}), sexp, yojson)] type chat_models = - | DeepSeek_V3 - | O3_Mini_High - | Gemini_Experimental_1206 - | Gemini_Flash_Lite; + | Gemini_Flash_Lite + | Llama_3_1_Nemo; [@deriving (show({with_path: false}), sexp, yojson)] type role = @@ -49,10 +47,8 @@ type reply = { [@deriving (show({with_path: false}), sexp, yojson)] let string_of_chat_model = fun - | DeepSeek_V3 => "deepseek-v3" - | O3_Mini_High => "o3-mini-high" - | Gemini_Experimental_1206 => "gemini-experimental-1206" - | Gemini_Flash_Lite => "google/gemini-2.0-flash-lite-preview-02-05:free"; + | Gemini_Flash_Lite => "google/gemini-2.0-flash-lite-preview-02-05:free" + | Llama_3_1_Nemo => "nvidia/llama-3.1-nemotron-70b-instruct:free"; let string_of_role = fun @@ -80,10 +76,8 @@ let body = (~params: params, messages: prompt): Json.t => { let lookup_key = (llm: chat_models) => switch (llm) { - | DeepSeek_V3 => Store.Generic.load("API") - | O3_Mini_High => Store.Generic.load("API") - | Gemini_Experimental_1206 => Store.Generic.load("API") - | Gemini_Flash_Lite => Store.Generic.load("API") // Adjust if using a different key + | Gemini_Flash_Lite => Store.Generic.load("API") + | Llama_3_1_Nemo => Store.Generic.load("API") }; let chat = (~key, ~body, ~handler): unit => @@ -106,10 +100,8 @@ let chat = (~key, ~body, ~handler): unit => let start_chat = (~params, ~key, prompt: prompt, handler): unit => { let body = body(~params, prompt); switch (params.llm) { - | DeepSeek_V3 => chat(~key, ~body, ~handler) - | O3_Mini_High => chat(~key, ~body, ~handler) - | Gemini_Experimental_1206 => chat(~key, ~body, ~handler) - | Gemini_Flash_Lite => chat(~key, ~body, ~handler) // Add Dolphin handling + | Gemini_Flash_Lite => chat(~key, ~body, ~handler) + | Llama_3_1_Nemo => chat(~key, ~body, ~handler) }; }; diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index e3c240c525..0326b79e5d 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -306,4 +306,30 @@ word-wrap: break-word; margin-bottom: -15px; overflow-x: auto; +} + +.llm-selector { + margin: auto; +} + +.llm-label { + font-weight: bold; + margin-right: 10px; +} + +.llm-dropdown { + padding: 5px; + border-radius: 4px; + border: 1px solid #ccc; + background-color: #fff; + cursor: pointer; +} + +.llm-dropdown:hover { + border-color: #888; +} + +.llm-dropdown:focus { + outline: none; + border-color: #555; } \ No newline at end of file From daa957a89401fe494c46d6e9b055c0fa5b39372e Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 13:56:49 -0500 Subject: [PATCH 32/50] properly displays current selected chat model in select menu --- src/haz3lweb/app/assistant/AssistantView.re | 22 +++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index a512df27e7..a888846447 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -121,8 +121,8 @@ let select_llm = let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); let selected_llm = switch (value) { - | "Gemini Flash Lite 2.0" => OpenRouter.Gemini_Flash_Lite - | "Llama 3.1 Nemotron 70B" => OpenRouter.Llama_3_1_Nemo + | "Gemini_Flash_Lite" => OpenRouter.Gemini_Flash_Lite + | "Llama_3_1_Nemo" => OpenRouter.Llama_3_1_Nemo | _ => OpenRouter.Gemini_Flash_Lite }; Virtual_dom.Vdom.Effect.Many([ @@ -131,6 +131,12 @@ let select_llm = ]); }; + // Helper function to determine if an option should be selected + let is_selected = + (llm: OpenRouter.chat_models, current_llm: OpenRouter.chat_models) => { + llm == current_llm; + }; + div( ~attrs=[clss(["llm-selector"])], [ @@ -139,11 +145,19 @@ let select_llm = ~attrs=[Attr.on_change(handle_change), clss(["llm-dropdown"])], [ option( - ~attrs=[Attr.value("Gemini_Flash_Lite")], + ~attrs=[ + Attr.value("Gemini_Flash_Lite"), + is_selected(OpenRouter.Gemini_Flash_Lite, assistantModel.llm) + ? Attr.selected : Attr.empty, + ], [text("Gemini Flash Lite 2.0")], ), option( - ~attrs=[Attr.value("Llama 3.1 Nemotron 70B")], + ~attrs=[ + Attr.value("Llama_3_1_Nemo"), + is_selected(OpenRouter.Llama_3_1_Nemo, assistantModel.llm) + ? Attr.selected : Attr.empty, + ], [text("Llama 3.1 Nemotron 70B")], ), ], From d8586bd963ba9694e6daae10145e52930c25b719 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 14 Feb 2025 15:28:46 -0500 Subject: [PATCH 33/50] adds two more models from OpenRouter --- src/haz3lweb/app/assistant/AssistantModel.re | 2 +- src/haz3lweb/app/assistant/AssistantView.re | 32 +++++++++++++++++--- src/haz3lweb/util/API/OpenRouter.re | 26 +++++++++++----- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/assistant/AssistantModel.re index ff4e513810..31f98c7752 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/assistant/AssistantModel.re @@ -30,7 +30,7 @@ module Model = { }; [@deriving (show({with_path: false}), sexp, yojson)] - let init: t = {chat: [], currSender: LS, llm: Gemini_Flash_Lite}; + let init: t = {chat: [], currSender: LS, llm: Gemini_Flash_Lite_2_0}; }; module Update = { diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/assistant/AssistantView.re index a888846447..f440bc8794 100644 --- a/src/haz3lweb/app/assistant/AssistantView.re +++ b/src/haz3lweb/app/assistant/AssistantView.re @@ -121,9 +121,11 @@ let select_llm = let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); let selected_llm = switch (value) { - | "Gemini_Flash_Lite" => OpenRouter.Gemini_Flash_Lite + | "Gemini_Flash_Lite_2_0" => OpenRouter.Gemini_Flash_Lite_2_0 + | "Gemini_Experimental_1206" => OpenRouter.Gemini_Experimental_1206 | "Llama_3_1_Nemo" => OpenRouter.Llama_3_1_Nemo - | _ => OpenRouter.Gemini_Flash_Lite + | "DeepSeek_V3" => OpenRouter.DeepSeek_V3 + | _ => OpenRouter.Gemini_Flash_Lite_2_0 }; Virtual_dom.Vdom.Effect.Many([ inject(AssistantModel.Update.SelectLLM(selected_llm)), @@ -146,12 +148,26 @@ let select_llm = [ option( ~attrs=[ - Attr.value("Gemini_Flash_Lite"), - is_selected(OpenRouter.Gemini_Flash_Lite, assistantModel.llm) + Attr.value("Gemini_Flash_Lite_2_0"), + is_selected( + OpenRouter.Gemini_Flash_Lite_2_0, + assistantModel.llm, + ) ? Attr.selected : Attr.empty, ], [text("Gemini Flash Lite 2.0")], ), + option( + ~attrs=[ + Attr.value("Gemini_Experimental_1206"), + is_selected( + OpenRouter.Gemini_Experimental_1206, + assistantModel.llm, + ) + ? Attr.selected : Attr.empty, + ], + [text("Gemini Experimental 1206")], + ), option( ~attrs=[ Attr.value("Llama_3_1_Nemo"), @@ -160,6 +176,14 @@ let select_llm = ], [text("Llama 3.1 Nemotron 70B")], ), + option( + ~attrs=[ + Attr.value("DeepSeek_V3"), + is_selected(OpenRouter.DeepSeek_V3, assistantModel.llm) + ? Attr.selected : Attr.empty, + ], + [text("DeepSeek V3")], + ), ], ), ], diff --git a/src/haz3lweb/util/API/OpenRouter.re b/src/haz3lweb/util/API/OpenRouter.re index d907418361..7e61d0a2df 100644 --- a/src/haz3lweb/util/API/OpenRouter.re +++ b/src/haz3lweb/util/API/OpenRouter.re @@ -5,8 +5,10 @@ open Util; [@deriving (show({with_path: false}), sexp, yojson)] type chat_models = - | Gemini_Flash_Lite - | Llama_3_1_Nemo; + | Gemini_Flash_Lite_2_0 + | Gemini_Experimental_1206 + | Llama_3_1_Nemo + | DeepSeek_V3; [@deriving (show({with_path: false}), sexp, yojson)] type role = @@ -47,8 +49,10 @@ type reply = { [@deriving (show({with_path: false}), sexp, yojson)] let string_of_chat_model = fun - | Gemini_Flash_Lite => "google/gemini-2.0-flash-lite-preview-02-05:free" - | Llama_3_1_Nemo => "nvidia/llama-3.1-nemotron-70b-instruct:free"; + | Gemini_Flash_Lite_2_0 => "google/gemini-2.0-flash-lite-preview-02-05:free" + | Gemini_Experimental_1206 => "google/gemini-exp-1206:free" + | Llama_3_1_Nemo => "nvidia/llama-3.1-nemotron-70b-instruct:free" + | DeepSeek_V3 => "deepseek/deepseek-chat:free"; let string_of_role = fun @@ -57,7 +61,11 @@ let string_of_role = | Assistant => "assistant" | Function => "function"; -let default_params = {llm: Gemini_Flash_Lite, temperature: 1.0, top_p: 1.0}; +let default_params = { + llm: Gemini_Flash_Lite_2_0, + temperature: 1.0, + top_p: 1.0, +}; let mk_message = ({role, content}) => `Assoc([ @@ -76,8 +84,10 @@ let body = (~params: params, messages: prompt): Json.t => { let lookup_key = (llm: chat_models) => switch (llm) { - | Gemini_Flash_Lite => Store.Generic.load("API") + | Gemini_Flash_Lite_2_0 => Store.Generic.load("API") + | Gemini_Experimental_1206 => Store.Generic.load("API") | Llama_3_1_Nemo => Store.Generic.load("API") + | DeepSeek_V3 => Store.Generic.load("API") }; let chat = (~key, ~body, ~handler): unit => @@ -100,8 +110,10 @@ let chat = (~key, ~body, ~handler): unit => let start_chat = (~params, ~key, prompt: prompt, handler): unit => { let body = body(~params, prompt); switch (params.llm) { - | Gemini_Flash_Lite => chat(~key, ~body, ~handler) + | Gemini_Flash_Lite_2_0 => chat(~key, ~body, ~handler) + | Gemini_Experimental_1206 => chat(~key, ~body, ~handler) | Llama_3_1_Nemo => chat(~key, ~body, ~handler) + | DeepSeek_V3 => chat(~key, ~body, ~handler) }; }; From 087802d358fbb010d75a0c8674e3674762b60f34 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 15 Feb 2025 16:31:25 -0500 Subject: [PATCH 34/50] adds ?? triggering --- src/haz3lweb/app/editors/Editors.re | 12 +++++- src/haz3lweb/app/editors/code/CodeEditable.re | 3 +- .../app/editors/mode/ExercisesMode.re | 9 ++++- .../AssistantModel.re | 23 +++++++++++ .../AssistantSettings.re | 0 .../AssistantView.re | 0 .../ChatLSP.re | 0 .../Oracle.re | 0 src/haz3lweb/view/ExerciseMode.re | 38 +++++++++++++++++-- src/haz3lweb/view/Page.re | 5 +++ src/haz3lweb/view/ScratchMode.re | 18 +++++++++ 11 files changed, 102 insertions(+), 6 deletions(-) rename src/haz3lweb/app/{assistant => helpful-assistant}/AssistantModel.re (91%) rename src/haz3lweb/app/{assistant => helpful-assistant}/AssistantSettings.re (100%) rename src/haz3lweb/app/{assistant => helpful-assistant}/AssistantView.re (100%) rename src/haz3lweb/app/{assistant => helpful-assistant}/ChatLSP.re (100%) rename src/haz3lweb/app/{assistant => helpful-assistant}/Oracle.re (100%) diff --git a/src/haz3lweb/app/editors/Editors.re b/src/haz3lweb/app/editors/Editors.re index fdd27b85d9..718413916b 100644 --- a/src/haz3lweb/app/editors/Editors.re +++ b/src/haz3lweb/app/editors/Editors.re @@ -75,12 +75,20 @@ module Update = { // Exercises | Exercises(ExercisesMode.Update.t); - let update = (~globals: Globals.t, ~schedule_action, action, model: Model.t) => { + let update = + ( + ~globals: Globals.t, + ~schedule_action: t => unit, + ~schedule_assistant_action: AssistantModel.Update.t => unit, + action, + model: Model.t, + ) => { switch (action, model) { | (Scratch(action), Scratch(m)) => let* scratch = ScratchMode.Update.update( ~schedule_action=a => schedule_action(Scratch(a)), + ~schedule_assistant_action, ~is_documentation=false, ~settings=globals.settings, action, @@ -92,6 +100,7 @@ module Update = { ScratchMode.Update.update( ~settings=globals.settings, ~schedule_action=a => schedule_action(Scratch(a)), + ~schedule_assistant_action, ~is_documentation=true, action, m, @@ -102,6 +111,7 @@ module Update = { ExercisesMode.Update.update( ~globals, ~schedule_action=a => schedule_action(Exercises(a)), + ~schedule_assistant_action, action, m, ); diff --git a/src/haz3lweb/app/editors/code/CodeEditable.re b/src/haz3lweb/app/editors/code/CodeEditable.re index e90635a3ee..2cdfab5c75 100644 --- a/src/haz3lweb/app/editors/code/CodeEditable.re +++ b/src/haz3lweb/app/editors/code/CodeEditable.re @@ -22,7 +22,7 @@ module Update = { let update = (~settings: Settings.t, action: t, model: Model.t): Updated.t(Model.t) => { - let perform = (action, model: Model.t) => + let perform = (action: Action.t, model: Model.t) => { Editor.Update.update( ~settings=settings.core, action, @@ -59,6 +59,7 @@ module Update = { }; }, ); + }; switch (action) { | Perform(action) => perform(action, model) | Undo => diff --git a/src/haz3lweb/app/editors/mode/ExercisesMode.re b/src/haz3lweb/app/editors/mode/ExercisesMode.re index 669df2de2a..995cc92f25 100644 --- a/src/haz3lweb/app/editors/mode/ExercisesMode.re +++ b/src/haz3lweb/app/editors/mode/ExercisesMode.re @@ -229,7 +229,13 @@ module Update = { }; let update = - (~globals: Globals.t, ~schedule_action, action: t, model: Model.t) => { + ( + ~globals: Globals.t, + ~schedule_action, + ~schedule_assistant_action: AssistantModel.Update.t => unit, + action: t, + model: Model.t, + ) => { switch (action) { | Exercise(action) => let current = List.nth(model.exercises, model.current); @@ -237,6 +243,7 @@ module Update = { ExerciseMode.Update.update( ~settings=globals.settings, ~schedule_action, + ~schedule_assistant_action, action, current, ); diff --git a/src/haz3lweb/app/assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re similarity index 91% rename from src/haz3lweb/app/assistant/AssistantModel.re rename to src/haz3lweb/app/helpful-assistant/AssistantModel.re index 31f98c7752..2351a5417f 100644 --- a/src/haz3lweb/app/assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -3,6 +3,7 @@ open Haz3lcore; open Util; open Util.OptUtil.Syntax; open Example; +open StringUtil; module CodeModel = CodeEditable.Model; @@ -98,6 +99,28 @@ module Update = { ); }; + let check_req = + ( + char: string, + schedule_action: t => unit, + {caret, relatives: {siblings, _}, _} as z: Zipper.t, + ) + : unit => { + switch (caret, Zipper.neighbor_monotiles(siblings)) { + | (Outer, (_, Some(_))) => + switch (Zipper.right_neighbor_monotile(siblings)) { + | Some(c) => c == "??" ? schedule_action(SendSketch) : () + | _ => () + } + | (Outer, (_, None)) => + switch (Zipper.left_neighbor_monotile(siblings)) { + | Some(c) => c == "??" ? schedule_action(SendSketch) : () + | _ => () + } + | _ => () + }; + }; + let update = ( ~settings: AssistantSettings.t, diff --git a/src/haz3lweb/app/assistant/AssistantSettings.re b/src/haz3lweb/app/helpful-assistant/AssistantSettings.re similarity index 100% rename from src/haz3lweb/app/assistant/AssistantSettings.re rename to src/haz3lweb/app/helpful-assistant/AssistantSettings.re diff --git a/src/haz3lweb/app/assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re similarity index 100% rename from src/haz3lweb/app/assistant/AssistantView.re rename to src/haz3lweb/app/helpful-assistant/AssistantView.re diff --git a/src/haz3lweb/app/assistant/ChatLSP.re b/src/haz3lweb/app/helpful-assistant/ChatLSP.re similarity index 100% rename from src/haz3lweb/app/assistant/ChatLSP.re rename to src/haz3lweb/app/helpful-assistant/ChatLSP.re diff --git a/src/haz3lweb/app/assistant/Oracle.re b/src/haz3lweb/app/helpful-assistant/Oracle.re similarity index 100% rename from src/haz3lweb/app/assistant/Oracle.re rename to src/haz3lweb/app/helpful-assistant/Oracle.re diff --git a/src/haz3lweb/view/ExerciseMode.re b/src/haz3lweb/view/ExerciseMode.re index d8bc55fafa..a1cab64fba 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -57,7 +57,13 @@ module Update = { | ResetExercise; let update = - (~settings: Settings.t, ~schedule_action as _, action, model: Model.t) + ( + ~settings: Settings.t, + ~schedule_action as _, + ~schedule_assistant_action: AssistantModel.Update.t => unit, + action, + model: Model.t, + ) : Updated.t(Model.t) => { let instructor_mode = settings.instructor_mode; switch (action) { @@ -71,6 +77,19 @@ module Update = { editor |> CodeEditable.Model.mk |> CodeEditable.Update.update(~settings, action); + switch (action) { + | Perform(a) => + switch (a) { + | Insert(char) => + AssistantModel.Update.check_req( + char, + schedule_assistant_action, + new_editor.editor.state.zipper, + ) + | _ => () + } + | _ => () + }; { ...model, editors: @@ -82,14 +101,27 @@ module Update = { }; | Editor(pos, MainEditor(action)) => switch (CodeSelectable.Update.convert_action(action)) { - | Some(action) => + | Some(a) => let editor = Exercise.main_editor_of_state(~selection=pos, model.editors); let* new_editor = // Hack[Matt]: put Editor.t into a CodeSelectable.t to use its update function editor |> CodeSelectable.Model.mk - |> CodeSelectable.Update.update(~settings, action); + |> CodeSelectable.Update.update(~settings, a); + switch (action) { + | Perform(a) => + switch (a) { + | Insert(char) => + AssistantModel.Update.check_req( + char, + schedule_assistant_action, + new_editor.editor.state.zipper, + ) + | _ => () + } + | _ => () + }; { ...model, editors: diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 133f16c28e..d2e8af2bbb 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -132,6 +132,7 @@ module Update = { Editors.Update.update( ~globals, ~schedule_action=a => schedule_action(Editors(a)), + ~schedule_assistant_action=a => schedule_action(Assistant(a)), action, model.editors, ); @@ -163,6 +164,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), + ~schedule_assistant_action=a => schedule_action(Assistant(a)), action, model.editors, ); @@ -181,6 +183,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), + ~schedule_assistant_action=a => schedule_action(Assistant(a)), action, model.editors, ); @@ -199,6 +202,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), + ~schedule_assistant_action=a => schedule_action(Assistant(a)), action, model.editors, ); @@ -228,6 +232,7 @@ module Update = { Editors.Update.update( ~globals, ~schedule_action=a => schedule_action(Editors(a)), + ~schedule_assistant_action=a => schedule_action(Assistant(a)), action, model.editors, ); diff --git a/src/haz3lweb/view/ScratchMode.re b/src/haz3lweb/view/ScratchMode.re index 91d26c6f93..ac0d01d491 100644 --- a/src/haz3lweb/view/ScratchMode.re +++ b/src/haz3lweb/view/ScratchMode.re @@ -86,6 +86,7 @@ module Update = { let update = ( ~schedule_action, + ~schedule_assistant_action: AssistantModel.Update.t => unit, ~settings: Settings.t, ~is_documentation: bool, action, @@ -95,6 +96,23 @@ module Update = { | CellAction(a) => let (key, ed) = List.nth(model.scratchpads, model.current); let* new_ed = CellEditor.Update.update(~settings, a, ed); + switch (a) { + | MainEditor(action) => + switch (action) { + | Perform(a) => + switch (a) { + | Insert(char) => + AssistantModel.Update.check_req( + char, + schedule_assistant_action, + new_ed.editor.editor.state.zipper, + ) + | _ => () + } + | _ => () + } + | _ => () + }; let new_sp = ListUtil.put_nth(model.current, (key, new_ed), model.scratchpads); {...model, scratchpads: new_sp}; From dd70ccc873f5c06a78366552af07a2d94ffd24ce Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 15 Feb 2025 16:49:02 -0500 Subject: [PATCH 35/50] adds chat collection and displays LLM response when requesting code completion --- .../app/helpful-assistant/AssistantModel.re | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re index 2351a5417f..b1799916f0 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -62,7 +62,7 @@ module Update = { let response_as_message: Model.message = { party: LLM, code: Some(segment_of_response), - content: "", + content: response, collapsed: String.length(response) >= 200, }; Respond(response_as_message); @@ -199,6 +199,14 @@ module Update = { openrouter_prompt, ); let prompt = ListUtil.concat_strings(messages); + let message: Model.message = { + party: LS, + code: Some(sketch_seg), + content: prompt, + collapsed: String.length(prompt) >= 200, + }; + let collected_chat = collect_chat(~messages=model.chat @ [message]); + print_endline(collected_chat); let llm = model.llm; let key = Store.Generic.load("API"); let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; @@ -211,17 +219,7 @@ module Update = { ); Model.{ ...model, - chat: - model.chat - @ [ - { - party: LS, - code: Some(sketch_seg), - content: prompt, - collapsed: String.length(prompt) >= 200, - }, - await_llm_response, - ], + chat: model.chat @ [message, await_llm_response], currSender: LLM, } |> Updated.return_quiet; From d399367c95382e3565e922edf1a7e677d595c6d4 Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Sat, 15 Feb 2025 19:31:40 -0500 Subject: [PATCH 36/50] added error rounds and fixed build error --- .../app/helpful-assistant/AssistantModel.re | 121 +++++++++++++++--- src/haz3lweb/app/helpful-assistant/ChatLSP.re | 7 +- 2 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re index 2351a5417f..fbea74b0c6 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -40,35 +40,50 @@ module Update = { | SendMessage(Model.message) | SetKey(string) | SendSketch + | SendError(string, Info.t, int) + | ErrorRespond(string, Info.t, int) | NewChat | Respond(Model.message) | ToggleCollapse(int) | SelectLLM(OpenRouter.chat_models); - let react = (~response: string, ~code_suggestion: bool): t => { - // let response = response |> sanitize_response |> quote; + let code_message_of_str = + (response: string, party: Model.party): Model.message => { let zipper_of_response = Printer.zipper_of_string(response); - let response_as_message: Model.message = { - party: LLM, + switch (zipper_of_response) { + | Some(z) => + let segment_of_response = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); + { + party, + code: Some(segment_of_response), + content: "", + collapsed: String.length(response) >= 200, + }; + | None => { + party, + code: None, + content: response, + collapsed: String.length(response) >= 200, + } + }; + }; + + let text_message_of_str = + (response: string, party: Model.party): Model.message => { + { + party, code: None, content: response, collapsed: String.length(response) >= 200, }; + }; + + let react = (~response: string, ~code_suggestion: bool): t => { + // let response = response |> sanitize_response |> quote; code_suggestion - ? switch (zipper_of_response) { - | Some(z) => - let segment_of_response = - Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); - let response_as_message: Model.message = { - party: LLM, - code: Some(segment_of_response), - content: "", - collapsed: String.length(response) >= 200, - }; - Respond(response_as_message); - | None => Respond(response_as_message) - } - : Respond(response_as_message); + ? Respond(code_message_of_str(response, LLM)) + : Respond(text_message_of_str(response, LLM)); }; let await_llm_response: Model.message = { @@ -101,9 +116,9 @@ module Update = { let check_req = ( - char: string, + _: string, schedule_action: t => unit, - {caret, relatives: {siblings, _}, _} as z: Zipper.t, + {caret, relatives: {siblings, _}, _}: Zipper.t, ) : unit => { switch (caret, Zipper.neighbor_monotiles(siblings)) { @@ -205,7 +220,17 @@ module Update = { OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => - schedule_action(react(~response=content, ~code_suggestion=true)) + let index = + Option.get(Indicated.index(editor.editor.state.zipper)); + let ci = + Option.get(Id.Map.find_opt(index, editor.statics.info_map)); + schedule_action( + ErrorRespond( + content, + ci, + ChatLSP.Options.init.error_rounds_max, + ), + ); | None => print_endline("Assistant: response parse failed") } ); @@ -226,6 +251,60 @@ module Update = { } |> Updated.return_quiet; }; + | ErrorRespond(response, ci, fuel) => + let message = code_message_of_str(response, LLM); + switch (ChatLSP.Prompt.mk_error(ci, response)) { + | None => + print_endline("ERROR ROUNDS (Non-error Response): " ++ response) + | Some(error) => + print_endline("ERROR ROUNDS (Error): " ++ error); + print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); + schedule_action(SendError(error, ci, fuel - 1)); + }; + Model.{ + ...model, + chat: ListUtil.leading(model.chat) @ [message], + currSender: LS, + } + |> Updated.return_quiet; + | SendError(error, ci, fuel) => + // check that fuel is not 0 + let error_message = + text_message_of_str( + "Your previous response caused the following error. Please fix it in your response: " + ++ error, + LS, + ); + if (fuel <= 0) { + schedule_action( + Respond( + text_message_of_str("Error round limit reached, stopping", LLM), + ), + ); + } else { + let collected_chat = + collect_chat(~messages=model.chat @ [error_message]); + switch (Oracle.ask(collected_chat)) { + | None => print_endline("Oracle: prompt generation failed") + | Some(prompt) => + let llm = model.llm; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action(ErrorRespond(content, ci, fuel)) + | None => print_endline("Assistant: response parse failed") + } + ); + }; + }; + Model.{ + ...model, + chat: model.chat @ [error_message, await_llm_response], + currSender: LLM, + } + |> Updated.return_quiet; | ToggleCollapse(index) => let updated_chat = List.mapi( diff --git a/src/haz3lweb/app/helpful-assistant/ChatLSP.re b/src/haz3lweb/app/helpful-assistant/ChatLSP.re index 2e3c9cd6ce..6603379224 100644 --- a/src/haz3lweb/app/helpful-assistant/ChatLSP.re +++ b/src/haz3lweb/app/helpful-assistant/ChatLSP.re @@ -722,6 +722,9 @@ module SystemPrompt = { let main_prompt = [ "CODE COMPLETION INSTRUCTIONS:", "- Reply with a functional, idiomatic replacement for the program hole marked '??' in the provided program sketch", + // Uncomment either of the following lines to test error rounds + //"- However, make sure that your initial response CAUSES A TYPE ERROR in the program. Then, fix it in your second response", + //"- However, make sure that your response ALWAYS CAUSES A TYPE ERROR in the program, no matter how many times you are re-prompted", "- Reply only with a single replacement term for the unqiue distinguished hole marked '??'", "- Reply only with code", "- DO NOT suggest more replacements for other holes in the sketch (marked '?'), or implicit holes", @@ -821,7 +824,7 @@ module Prompt = { @ [{role: User, content: user_message}]; }; - let mk_error = (ci: Info.t, reply: OpenRouter.reply): option(string) => { + let mk_error = (ci: Info.t, reply: string): option(string) => { /* TODO: This should maybe take whole JSON convo * so far and return an appended version */ //TODO: Proper errors @@ -832,6 +835,6 @@ module Prompt = { | _ => None }; let init_ctx = Info.ctx_of(ci); - ErrorPrint.mk(~init_ctx, ~mode, reply.content); + ErrorPrint.mk(~init_ctx, ~mode, reply); }; }; From 424610fea0a8de7957029a6351483033a56fd6ae Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Tue, 18 Feb 2025 12:56:51 -0500 Subject: [PATCH 37/50] suggests llm code; this is kind of buggy in certain cases... nevertheless, it works if you hit tab-- mainly the UI and some sort of tile parsing/filling when filling holes in between tiles --- src/haz3lcore/assistant/TyDi.re | 18 +++++++++- src/haz3lcore/zipper/Selection.re | 3 ++ src/haz3lcore/zipper/Zipper.re | 5 +++ src/haz3lcore/zipper/action/Action.re | 3 +- src/haz3lcore/zipper/action/Perform.re | 12 +++++-- .../app/helpful-assistant/AssistantModel.re | 34 ++++++++++++++++--- 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/haz3lcore/assistant/TyDi.re b/src/haz3lcore/assistant/TyDi.re index 1118fc115f..4d04174639 100644 --- a/src/haz3lcore/assistant/TyDi.re +++ b/src/haz3lcore/assistant/TyDi.re @@ -68,12 +68,13 @@ let suffix_of = (candidate: Token.t, current: Token.t): option(Token.t) => { }; /* Returns the text content of the suggestion buffer */ -let get_buffer = (z: Zipper.t): option(Token.t) => +let get_buffer = (z: Zipper.t): option(Token.t) => { switch (z.selection.mode, z.selection.content) { | (Buffer(Unparsed), [Tile({label: [completion], _})]) => Some(completion) | _ => None }; +}; /* Populates the suggestion buffer with a type-directed suggestion */ let set_buffer = (~info_map: Statics.Map.t, z: Zipper.t): option(Zipper.t) => { @@ -97,3 +98,18 @@ let set_buffer = (~info_map: Statics.Map.t, z: Zipper.t): option(Zipper.t) => { let z = Zipper.set_buffer(z, ~content, ~mode=Unparsed); Some(z); }; + +let set_llm_buffer = + (~info_map: Statics.Map.t, z: Zipper.t, response: string) + : option(Zipper.t) => { + let* index = Indicated.index(z); + let* ci = Id.Map.find_opt(index, info_map); + let content = + mk_unparsed_buffer( + ~sort=Info.sort_of(ci), + z.relatives.siblings, + response, + ); + let z = Zipper.set_llm_buffer(z, ~content, ~mode=Unparsed); + Some(z); +}; diff --git a/src/haz3lcore/zipper/Selection.re b/src/haz3lcore/zipper/Selection.re index 92d6509fb8..7459987caa 100644 --- a/src/haz3lcore/zipper/Selection.re +++ b/src/haz3lcore/zipper/Selection.re @@ -26,6 +26,9 @@ let mk = (~mode=Normal, ~focus=Direction.Left, content: Segment.t) => { let mk_buffer = buffer => mk(~mode=Buffer(buffer), ~focus=Direction.Left); +let mk_llm_buffer = buffer => + mk(~mode=Buffer(buffer), ~focus=Direction.Right); + let is_buffer: t => bool = fun | {mode: Buffer(_), _} => true diff --git a/src/haz3lcore/zipper/Zipper.re b/src/haz3lcore/zipper/Zipper.re index f87fb964e6..299d04ef67 100644 --- a/src/haz3lcore/zipper/Zipper.re +++ b/src/haz3lcore/zipper/Zipper.re @@ -326,6 +326,11 @@ let set_buffer = (z: t, ~mode: Selection.buffer, ~content: Segment.t): t => { selection: Selection.mk_buffer(mode, content), }; +let set_llm_buffer = (z: t, ~mode: Selection.buffer, ~content: Segment.t): t => { + ...z, + selection: Selection.mk_llm_buffer(mode, content), +}; + let is_linebreak_to_right_of_caret = ({relatives: {siblings: (_, r), _}, _}: t): bool => { switch (r) { diff --git a/src/haz3lcore/zipper/action/Action.re b/src/haz3lcore/zipper/action/Action.re index 6d9d3a36f5..4dcc1e724f 100644 --- a/src/haz3lcore/zipper/action/Action.re +++ b/src/haz3lcore/zipper/action/Action.re @@ -59,7 +59,8 @@ type project = [@deriving (show({with_path: false}), sexp, yojson)] type agent = - | TyDi; + | TyDi + | LLMSug(string); [@deriving (show({with_path: false}), sexp, yojson)] type buffer = diff --git a/src/haz3lcore/zipper/action/Perform.re b/src/haz3lcore/zipper/action/Perform.re index 89c9059ac1..64a07f4f1b 100644 --- a/src/haz3lcore/zipper/action/Perform.re +++ b/src/haz3lcore/zipper/action/Perform.re @@ -7,12 +7,18 @@ let buffer_clear = (z: t): t => | _ => z }; -let set_buffer = (info_map: Statics.Map.t, z: t): t => +let set_tydi_buffer = (info_map: Statics.Map.t, z: t): t => switch (TyDi.set_buffer(~info_map, z)) { | None => z | Some(z) => z }; +let set_llm_buffer = (info_map: Statics.Map.t, z: t, response: string): t => + switch (TyDi.set_llm_buffer(~info_map, z, response)) { + | None => z + | Some(z) => z + }; + let go_z = ( ~settings as _: CoreSettings.t, @@ -98,7 +104,9 @@ let go_z = | None => Error(CantReparse) | Some(z) => Ok(z) } - | Buffer(Set(TyDi)) => Ok(set_buffer(statics.info_map, z)) + | Buffer(Set(TyDi)) => Ok(set_tydi_buffer(statics.info_map, z)) + | Buffer(Set(LLMSug(response))) => + Ok(set_llm_buffer(statics.info_map, z, response)) | Buffer(Accept) => switch (buffer_accept(z)) { | None => Error(CantAccept) diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re index fdb74db9b2..bbceeb3963 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -53,7 +53,7 @@ module Update = { | ToggleCollapse(int) | SelectLLM(OpenRouter.chat_models) | StoreTile(Id.t) - | RemoveTile; + | RemoveAndSuggest(string); let code_message_of_str = (settings, editor: CodeModel.t, response: string, party: Model.party) @@ -180,6 +180,18 @@ module Update = { }; }; + let set_buffer = (~response: string, z: Zipper.t): option(Zipper.t) => { + let zipper_of_response = Option.get(Printer.zipper_of_string(response)); + let seg_of_response = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + zipper_of_response, + ); + let z = Zipper.set_buffer(z, ~content=seg_of_response, ~mode=Unparsed); + Some(z); + }; + let update = ( ~settings, @@ -304,7 +316,7 @@ module Update = { switch (ChatLSP.Prompt.mk_error(ci, response)) { | None => print_endline("ERROR ROUNDS (Non-error Response): " ++ response); - schedule_action(RemoveTile); + schedule_action(RemoveAndSuggest(response)); | Some(error) => print_endline("ERROR ROUNDS (Error): " ++ error); print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); @@ -368,8 +380,7 @@ module Update = { Model.{...model, chat: updated_chat} |> Updated.return_quiet; | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet | StoreTile(id) => {...model, tile: id} |> Updated.return_quiet - | RemoveTile => - print_endline("Here now"); + | RemoveAndSuggest(response) => // Select Question Marks and double-destruct let perform_action: CodeEditable.Update.t = Perform(Action.Select(Tile(Id(model.tile, Direction.Left)))); @@ -381,6 +392,21 @@ module Update = { let cell_action: CellEditor.Update.t = MainEditor(perform_action); let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); schedule_editor_action(scratch_action); + + let perform_action: CodeEditable.Update.t = + Perform(Action.Buffer(Set(LLMSug(response)))); + let cell_action: CellEditor.Update.t = MainEditor(perform_action); + let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); + schedule_editor_action(scratch_action); + + /* Paste in code completion + let perform_action: CodeEditable.Update.t = + Perform(Action.Paste(response)); + let cell_action: CellEditor.Update.t = MainEditor(perform_action); + let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); + schedule_editor_action(scratch_action); + */ + {...model, tile: Id.invalid} |> Updated.return_quiet; }; }; From 6f01e7393dbc530987104f182c4dc04b8ee4c19b Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Mon, 24 Feb 2025 09:39:18 -0500 Subject: [PATCH 38/50] removes error highlighting from ?? or token-holes; gives rainbow animation to ?? --- src/haz3lcore/lang/Form.re | 10 ++++ src/haz3lcore/statics/MakeTerm.re | 15 +++-- src/haz3lweb/app/editors/code/Code.re | 1 + .../app/helpful-assistant/AssistantModel.re | 42 ++++++-------- .../app/helpful-assistant/AssistantView.re | 57 +++++-------------- src/haz3lweb/www/style/editor.css | 16 ++++++ 6 files changed, 70 insertions(+), 71 deletions(-) diff --git a/src/haz3lcore/lang/Form.re b/src/haz3lcore/lang/Form.re index 512962e778..c751505cac 100644 --- a/src/haz3lcore/lang/Form.re +++ b/src/haz3lcore/lang/Form.re @@ -202,7 +202,10 @@ let const_mono_delims = base_typs @ bools @ [undefined, wild, empty_list, empty_tuple, empty_string]; let explicit_hole = "?"; +let llm_hole = "??"; let is_explicit_hole = t => t == explicit_hole; + +let is_llm_hole = t => t == llm_hole; let bad_token_cls: string => bad_token_cls = t => switch () { @@ -222,6 +225,13 @@ let atomic_forms: list((string, (string => bool, list(Mold.t)))) = [ [mk_op(Exp, []), mk_op(Pat, []), mk_op(Typ, []), mk_op(TPat, [])], ), ), + ( + "llm_hole", + ( + is_llm_hole, + [mk_op(Exp, []), mk_op(Pat, []), mk_op(Typ, []), mk_op(TPat, [])], + ), + ), ("wild", (is_wild, [mk_op(Pat, [])])), ("string", (is_string, [mk_op(Exp, []), mk_op(Pat, [])])), ("int_lit", (is_int, [mk_op(Exp, []), mk_op(Pat, [])])), diff --git a/src/haz3lcore/statics/MakeTerm.re b/src/haz3lcore/statics/MakeTerm.re index 9c28029e00..8d7a17a5a8 100644 --- a/src/haz3lcore/statics/MakeTerm.re +++ b/src/haz3lcore/statics/MakeTerm.re @@ -202,7 +202,8 @@ and exp_term: unsorted => (Exp.term, list(Id.t)) = { Match(scrut, rules), ids, ) - | ([t], []) when t != " " && !Form.is_explicit_hole(t) => + | ([t], []) + when t != " " && !Form.is_explicit_hole(t) && !Form.is_llm_hole(t) => ret(Invalid(t)) | _ => ret(hole(tm)) } @@ -351,7 +352,9 @@ and pat_term: unsorted => (Pat.term, list(Id.t)) = { | ([t], []) when Form.is_wild(t) => Wild | ([t], []) when Form.is_ctr(t) => Constructor(t, Unknown(Internal) |> Typ.fresh) - | ([t], []) when t != " " && !Form.is_explicit_hole(t) => + | ([t], []) + when + t != " " && !Form.is_explicit_hole(t) && !Form.is_llm_hole(t) => Invalid(t) | (["(", ")"], [Pat(body)]) => Parens(body) | (["[", "]"], [Pat(body)]) => @@ -415,7 +418,9 @@ and typ_term: unsorted => (Typ.term, list(Id.t)) = { | ([t], []) when Form.is_typ_var(t) => Var(t) | (["(", ")"], [Typ(body)]) => Parens(body) | (["[", "]"], [Typ(body)]) => List(body) - | ([t], []) when t != " " && !Form.is_explicit_hole(t) => + | ([t], []) + when + t != " " && !Form.is_explicit_hole(t) && !Form.is_llm_hole(t) => Unknown(Hole(Invalid(t))) | _ => hole(tm) }, @@ -483,7 +488,9 @@ and tpat_term: unsorted => TPat.term = { ret( switch (tile) { | ([t], []) when Form.is_typ_var(t) => Var(t) - | ([t], []) when t != " " && !Form.is_explicit_hole(t) => + | ([t], []) + when + t != " " && !Form.is_explicit_hole(t) && !Form.is_llm_hole(t) => Invalid(t) | _ => hole(tm) }, diff --git a/src/haz3lweb/app/editors/code/Code.re b/src/haz3lweb/app/editors/code/Code.re index ee0cd2b564..fc261ca0d8 100644 --- a/src/haz3lweb/app/editors/code/Code.re +++ b/src/haz3lweb/app/editors/code/Code.re @@ -15,6 +15,7 @@ let of_delim' = | _ when is_in_buffer => "in-buffer" | _ when !is_consistent => "sort-inconsistent" | _ when !is_complete => "incomplete" + | [s] when s == Form.llm_hole => "llm-waiting" | [s] when s == Form.explicit_hole => "explicit-hole" | [s] when Form.is_string(s) => "string-lit" | _ => Sort.to_string(sort) diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re index bbceeb3963..776fae8259 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -2,7 +2,6 @@ module Sexp = Sexplib.Sexp; open Haz3lcore; open Util; open Util.OptUtil.Syntax; -open Example; open StringUtil; module CodeModel = CodeEditable.Model; @@ -381,31 +380,24 @@ module Update = { | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet | StoreTile(id) => {...model, tile: id} |> Updated.return_quiet | RemoveAndSuggest(response) => - // Select Question Marks and double-destruct - let perform_action: CodeEditable.Update.t = - Perform(Action.Select(Tile(Id(model.tile, Direction.Left)))); - let cell_action: CellEditor.Update.t = MainEditor(perform_action); - let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); - schedule_editor_action(scratch_action); - let perform_action: CodeEditable.Update.t = - Perform(Action.Destruct(Direction.Left)); - let cell_action: CellEditor.Update.t = MainEditor(perform_action); - let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); - schedule_editor_action(scratch_action); + // Create a sequence of actions to handle the suggestion + let actions = [ + Action.Select(Tile(Id(model.tile, Direction.Left))), + Action.Destruct(Direction.Left), + Action.Buffer(Set(LLMSug(response))), + ]; - let perform_action: CodeEditable.Update.t = - Perform(Action.Buffer(Set(LLMSug(response)))); - let cell_action: CellEditor.Update.t = MainEditor(perform_action); - let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); - schedule_editor_action(scratch_action); - - /* Paste in code completion - let perform_action: CodeEditable.Update.t = - Perform(Action.Paste(response)); - let cell_action: CellEditor.Update.t = MainEditor(perform_action); - let scratch_action: EditorsUpdate.t = Scratch(CellAction(cell_action)); - schedule_editor_action(scratch_action); - */ + // Apply each action in sequence + List.iter( + action => { + let perform_action = CodeEditable.Update.Perform(action); + let cell_action = CellEditor.Update.MainEditor(perform_action); + let scratch_action = + EditorsUpdate.Scratch(CellAction(cell_action)); + schedule_editor_action(scratch_action); + }, + actions, + ); {...model, tile: Id.invalid} |> Updated.return_quiet; }; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index f0a9a28a00..d657f43c4d 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -1,10 +1,9 @@ module Sexp = Sexplib.Sexp; +open Haz3lcore; open Virtual_dom.Vdom; open Node; open Util.Web; open Util; -open Util.OptUtil.Syntax; -open Haz3lcore; open Js_of_ocaml; type selection = @@ -69,7 +68,7 @@ let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; -let resume_chat_button = (~globals: Globals.t, ~inject): Node.t => { +let resume_chat_button = (~globals: Globals.t): Node.t => { let tooltip = "Previous Chat"; let resume_chat = _ => Virtual_dom.Vdom.Effect.Many([ @@ -82,7 +81,7 @@ let resume_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; -let req_button = (~globals: Globals.t, ~inject): Node.t => { +let req_button = (~inject): Node.t => { let tooltip = "??"; let send_sketch = _ => Virtual_dom.Vdom.Effect.Many([ @@ -95,7 +94,7 @@ let req_button = (~globals: Globals.t, ~inject): Node.t => { ); }; -let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { +let end_chat_button = (~globals: Globals.t): Node.t => { let tooltip = "End Chat"; let end_chat = _ => Virtual_dom.Vdom.Effect.Many([ @@ -108,14 +107,7 @@ let end_chat_button = (~globals: Globals.t, ~inject): Node.t => { ); }; -let select_llm = - ( - ~signal, - ~inject, - ~globals: Globals.t, - ~assistantModel: AssistantModel.Model.t, - ) - : Node.t => { +let select_llm = (~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { let handle_change = (event, _) => { let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); let selected_llm = @@ -196,19 +188,13 @@ let settings_box = (~globals: Globals.t, ~inject): Node.t => { // llm_toggle(~globals), // lsp_toggle(~globals), begin_chat_button(~globals, ~inject), - resume_chat_button(~globals, ~inject), + resume_chat_button(~globals), ], ); }; let api_input = - ( - ~signal, - ~inject, - ~globals: Globals.t, - ~assistantModel: AssistantModel.Model.t, - ) - : Node.t => { + (~signal, ~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { let handle_submission = (api_key: string) => { JsUtil.log("Your API key for this session has been set: " ++ api_key); Virtual_dom.Vdom.Effect.Many([ @@ -280,13 +266,7 @@ let api_input = }; let message_input = - ( - ~signal, - ~inject, - ~globals: Globals.t, - ~assistantModel: AssistantModel.Model.t, - ) - : Node.t => { + (~signal, ~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { let handle_send = (message: string) => { let message: AssistantModel.Model.message = { party: assistantModel.currSender, @@ -378,12 +358,7 @@ let loading_dots = () => { }; let message_display = - ( - ~signal, - ~inject, - ~globals: Globals.t, - ~assistantModel: AssistantModel.Model.t, - ) + (~inject, ~globals: Globals.t, ~assistantModel: AssistantModel.Model.t) : Node.t => { let toggle_collapse = index => { // Create an action to toggle the collapsed state of a specific message @@ -540,21 +515,19 @@ let view = [text("Agentic Assistant Chat")], ), globals.settings.assistant.ongoing_chat - ? req_button(~globals, ~inject) : None, + ? req_button(~inject) : None, globals.settings.assistant.ongoing_chat - ? end_chat_button(~globals, ~inject) : None, + ? end_chat_button(~globals) : None, ], ), globals.settings.assistant.ongoing_chat - ? message_display(~signal, ~inject, ~globals, ~assistantModel) - : None, + ? message_display(~inject, ~globals, ~assistantModel) : None, globals.settings.assistant.ongoing_chat - ? message_input(~signal, ~inject, ~globals, ~assistantModel) - : None, + ? message_input(~signal, ~inject, ~assistantModel) : None, globals.settings.assistant.ongoing_chat - ? None : api_input(~signal, ~inject, ~globals, ~assistantModel), + ? None : api_input(~signal, ~inject, ~assistantModel), globals.settings.assistant.ongoing_chat - ? None : select_llm(~signal, ~inject, ~globals, ~assistantModel), + ? None : select_llm(~inject, ~assistantModel), globals.settings.assistant.ongoing_chat ? None : settings_box(~globals, ~inject), ], diff --git a/src/haz3lweb/www/style/editor.css b/src/haz3lweb/www/style/editor.css index 13ac029688..816cd49048 100644 --- a/src/haz3lweb/www/style/editor.css +++ b/src/haz3lweb/www/style/editor.css @@ -72,6 +72,7 @@ .code .token.explicit-hole { color: var(--token-explicit-hole); text-shadow: 1px 1px 0px var(--token-explicit-hole-shadow); + font-weight: bold; } .code .comment { @@ -89,6 +90,21 @@ font-size: 0.59em; } +.code .token.llm-waiting { + animation: rainbow 4s linear infinite; + font-weight: bold; +} + +@keyframes rainbow { + 0% { color: #4477ff; } /* Start with muted blue */ + 17% { color: #9944ff; } /* Muted purple */ + 33% { color: #ff4488; } /* Muted pink */ + 50% { color: #ff7744; } /* Muted orange */ + 67% { color: #88aa44; } /* Muted green */ + 83% { color: #44aaff; } /* Muted cyan */ + 100% { color: #4477ff; } /* Back to muted blue */ +} + /* TOKEN BACKING DECOS */ svg.shard > path { From 97784c7c7913a05940890fc5f85e0c8bdfd8cb7a Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Thu, 27 Feb 2025 11:51:38 -0500 Subject: [PATCH 39/50] fixes dep cycles through dep inversion; define outer functions to be passed through update calls, starting in Page.re --- src/haz3lweb/app/editors/Editors.re | 115 ++++++++++-------- src/haz3lweb/app/editors/EditorsModel.re | 11 -- src/haz3lweb/app/editors/EditorsUpdate.re | 7 -- .../app/editors/mode/ExercisesMode.re | 57 +++++---- .../app/editors/mode/ExercisesModeModel.re | 13 -- .../app/editors/mode/ExercisesModeUpdate.re | 10 -- .../app/helpful-assistant/AssistantModel.re | 22 +--- src/haz3lweb/debug/Benchmark.re | 2 +- src/haz3lweb/view/ExerciseMode.re | 65 +++------- src/haz3lweb/view/ExerciseModeModel.re | 17 --- src/haz3lweb/view/ExerciseModeUpdate.re | 9 -- src/haz3lweb/view/Page.re | 52 ++++++-- src/haz3lweb/view/ScratchMode.re | 47 ++++--- src/haz3lweb/view/ScratchModeModel.re | 11 -- src/haz3lweb/view/ScratchModeUpdate.re | 11 -- 15 files changed, 184 insertions(+), 265 deletions(-) delete mode 100644 src/haz3lweb/app/editors/EditorsModel.re delete mode 100644 src/haz3lweb/app/editors/EditorsUpdate.re delete mode 100644 src/haz3lweb/app/editors/mode/ExercisesModeModel.re delete mode 100644 src/haz3lweb/app/editors/mode/ExercisesModeUpdate.re delete mode 100644 src/haz3lweb/view/ExerciseModeModel.re delete mode 100644 src/haz3lweb/view/ExerciseModeUpdate.re delete mode 100644 src/haz3lweb/view/ScratchModeModel.re delete mode 100644 src/haz3lweb/view/ScratchModeUpdate.re diff --git a/src/haz3lweb/app/editors/Editors.re b/src/haz3lweb/app/editors/Editors.re index f9365d534a..b396057221 100644 --- a/src/haz3lweb/app/editors/Editors.re +++ b/src/haz3lweb/app/editors/Editors.re @@ -1,9 +1,15 @@ module Model = { [@deriving (show({with_path: false}), sexp, yojson)] - type mode = EditorsModel.mode; + type mode = + | Scratch + | Documentation + | Exercises; [@deriving (show({with_path: false}), sexp, yojson)] - type t = EditorsModel.t; + type t = + | Scratch(ScratchMode.Model.t) + | Documentation(ScratchMode.Model.t) + | Exercises(ExercisesMode.Model.t); let mode_string: t => string = fun @@ -25,16 +31,16 @@ module Store = { let mode = StoreMode.load(); switch (mode) { | Scratch => - EditorsModel.Scratch( + Model.Scratch( ScratchMode.Store.load() |> ScratchMode.Model.unpersist(~settings), ) | Documentation => - EditorsModel.Documentation( + Model.Documentation( ScratchMode.StoreDocumentation.load() |> ScratchMode.Model.unpersist_documentation(~settings), ) | Exercises => - EditorsModel.Exercises( + Model.Exercises( ExercisesMode.Store.load(~settings, ~instructor_mode) |> ExercisesMode.Model.unpersist(~settings, ~instructor_mode), ) @@ -43,15 +49,15 @@ module Store = { let save = (~instructor_mode, model: Model.t) => { switch (model) { - | EditorsModel.Scratch(m) => + | Model.Scratch(m) => StoreMode.save(Scratch); ScratchMode.Store.save(ScratchMode.Model.persist(m)); - | EditorsModel.Documentation(m) => + | Model.Documentation(m) => StoreMode.save(Documentation); ScratchMode.StoreDocumentation.save( ScratchMode.Model.persist_documentation(m), ); - | EditorsModel.Exercises(m) => + | Model.Exercises(m) => StoreMode.save(Exercises); ExercisesMode.Store.save(~instructor_mode, m); }; @@ -61,14 +67,20 @@ module Store = { module Update = { open Updated; - type t = EditorsUpdate.t; + [@deriving (show({with_path: false}), sexp, yojson)] + type t = + | SwitchMode(Model.mode) + // Scratch & Documentation + | Scratch(ScratchMode.Update.t) + // Exercises + | Exercises(ExercisesMode.Update.t); let update = ( ~globals: Globals.t, ~schedule_action: t => unit, - ~schedule_assistant_action: AssistantModel.Update.t => unit, - action: t, + ~send_insertion_info, + action, model: Model.t, ) => { switch (action, model) { @@ -76,34 +88,33 @@ module Update = { let* scratch = ScratchMode.Update.update( ~schedule_action=a => schedule_action(Scratch(a)), - ~schedule_assistant_action, + ~send_insertion_info, ~is_documentation=false, ~settings=globals.settings, action, m, ); - EditorsModel.Scratch(scratch); + Model.Scratch(scratch); | (Scratch(action), Documentation(m)) => let* scratch = ScratchMode.Update.update( ~settings=globals.settings, ~schedule_action=a => schedule_action(Scratch(a)), - ~schedule_assistant_action, + ~send_insertion_info, ~is_documentation=true, action, m, ); - EditorsModel.Documentation(scratch); + Model.Documentation(scratch); | (Exercises(action), Exercises(m)) => let* exercises = ExercisesMode.Update.update( ~globals, ~schedule_action=a => schedule_action(Exercises(a)), - ~schedule_assistant_action, action, m, ); - EditorsModel.Exercises(exercises); + Model.Exercises(exercises); | (Scratch(_), Exercises(_)) | (Exercises(_), Scratch(_)) | (Exercises(_), Documentation(_)) => model |> return_quiet @@ -111,13 +122,13 @@ module Update = { | (SwitchMode(Documentation), Documentation(_)) | (SwitchMode(Exercises), Exercises(_)) => model |> return_quiet | (SwitchMode(Scratch), _) => - EditorsModel.Scratch( + Model.Scratch( ScratchMode.Store.load() |> ScratchMode.Model.unpersist(~settings=globals.settings.core), ) |> return | (SwitchMode(Documentation), _) => - EditorsModel.Documentation( + Model.Documentation( ScratchMode.StoreDocumentation.load() |> ScratchMode.Model.unpersist_documentation( ~settings=globals.settings.core, @@ -125,7 +136,7 @@ module Update = { ) |> return | (SwitchMode(Exercises), _) => - EditorsModel.Exercises( + Model.Exercises( ExercisesMode.Store.load( ~settings=globals.settings.core, ~instructor_mode=globals.settings.instructor_mode, @@ -139,10 +150,10 @@ module Update = { }; }; - let calculate = (~settings, ~is_edited, ~schedule_action: t => unit, model) => { + let calculate = (~settings, ~is_edited, ~schedule_action, model) => { switch (model) { - | EditorsModel.Scratch(m) => - EditorsModel.Scratch( + | Model.Scratch(m) => + Model.Scratch( ScratchMode.Update.calculate( ~schedule_action=a => schedule_action(Scratch(a)), ~settings, @@ -150,8 +161,8 @@ module Update = { m, ), ) - | EditorsModel.Documentation(m) => - EditorsModel.Documentation( + | Model.Documentation(m) => + Model.Documentation( ScratchMode.Update.calculate( ~schedule_action=a => schedule_action(Scratch(a)), ~settings, @@ -159,8 +170,8 @@ module Update = { m, ), ) - | EditorsModel.Exercises(m) => - EditorsModel.Exercises( + | Model.Exercises(m) => + Model.Exercises( ExercisesMode.Update.calculate( ~schedule_action=a => schedule_action(Exercises(a)), ~settings, @@ -183,13 +194,13 @@ module Selection = { switch (selection, editors) { | (Scratch(selection), Scratch(m)) => let+ ci = ScratchMode.Selection.get_cursor_info(~selection, m); - EditorsUpdate.Scratch(ci); + Update.Scratch(ci); | (Scratch(selection), Documentation(m)) => let+ ci = ScratchMode.Selection.get_cursor_info(~selection, m); - EditorsUpdate.Scratch(ci); + Update.Scratch(ci); | (Exercises(selection), Exercises(m)) => let+ ci = ExercisesMode.Selection.get_cursor_info(~selection, m); - EditorsUpdate.Exercises(ci); + Update.Exercises(ci); | (Scratch(_), Exercises(_)) | (Exercises(_), Scratch(_)) | (Exercises(_), Documentation(_)) => empty @@ -201,13 +212,13 @@ module Selection = { switch (selection, editors) { | (Some(Scratch(selection)), Scratch(m)) => ScratchMode.Selection.handle_key_event(~selection, ~event, m) - |> Option.map(x => EditorsUpdate.Scratch(x)) + |> Option.map(x => Update.Scratch(x)) | (Some(Scratch(selection)), Documentation(m)) => ScratchMode.Selection.handle_key_event(~selection, ~event, m) - |> Option.map(x => EditorsUpdate.Scratch(x)) + |> Option.map(x => Update.Scratch(x)) | (Some(Exercises(selection)), Exercises(m)) => ExercisesMode.Selection.handle_key_event(~selection, ~event, m) - |> Option.map(x => EditorsUpdate.Exercises(x)) + |> Option.map(x => Update.Exercises(x)) | (Some(Scratch(_)), Exercises(_)) | (Some(Exercises(_)), Scratch(_)) | (Some(Exercises(_)), Documentation(_)) @@ -220,24 +231,21 @@ module Selection = { switch (model) { | Scratch(m) => ScratchMode.Selection.jump_to_tile(tile, m) - |> Option.map(((x, y)) => (EditorsUpdate.Scratch(x), Scratch(y))) + |> Option.map(((x, y)) => (Update.Scratch(x), Scratch(y))) | Documentation(m) => ScratchMode.Selection.jump_to_tile(tile, m) - |> Option.map(((x, y)) => (EditorsUpdate.Scratch(x), Scratch(y))) + |> Option.map(((x, y)) => (Update.Scratch(x), Scratch(y))) | Exercises(m) => ExercisesMode.Selection.jump_to_tile(~settings, tile, m) - |> Option.map(((x, y)) => - (EditorsUpdate.Exercises(x), Exercises(y)) - ) + |> Option.map(((x, y)) => (Update.Exercises(x), Exercises(y))) }; let default_selection = fun - | EditorsModel.Scratch(_) => - Scratch(ScratchMode.Selection.Cell(MainEditor)) - | EditorsModel.Documentation(_) => + | Model.Scratch(_) => Scratch(ScratchMode.Selection.Cell(MainEditor)) + | Model.Documentation(_) => Scratch(ScratchMode.Selection.Cell(MainEditor)) - | EditorsModel.Exercises(_) => Exercises((Exercise.Prelude, MainEditor)); + | Model.Exercises(_) => Exercises((Exercise.Prelude, MainEditor)); }; module View = { @@ -267,7 +275,7 @@ module View = { | Some(Scratch(s)) => Some(s) | _ => None }, - ~inject=a => EditorsUpdate.Scratch(a) |> inject, + ~inject=a => Update.Scratch(a) |> inject, m, ) | Documentation(m) => @@ -281,7 +289,7 @@ module View = { | Some(Scratch(s)) => Some(s) | _ => None }, - ~inject=a => EditorsUpdate.Scratch(a) |> inject, + ~inject=a => Update.Scratch(a) |> inject, m, ) | Exercises(m) => @@ -295,7 +303,7 @@ module View = { | Some(Exercises(s)) => Some(s) | _ => None }, - ~inject=a => EditorsUpdate.Exercises(a) |> inject, + ~inject=a => Update.Exercises(a) |> inject, m, ) }; @@ -306,13 +314,13 @@ module View = { | Documentation(s) => ScratchMode.View.file_menu( ~globals, - ~inject=x => inject(EditorsUpdate.Scratch(x)), + ~inject=x => inject(Update.Scratch(x)), s, ) | Exercises(e) => ExercisesMode.View.file_menu( ~globals, - ~inject=x => inject(EditorsUpdate.Exercises(x)), + ~inject=x => inject(Update.Exercises(x)), e, ) }; @@ -327,10 +335,9 @@ module View = { ~attrs=[ Attr.on_change(_ => fun - | "Scratch" => inject(EditorsUpdate.SwitchMode(Scratch)) - | "Documentation" => - inject(EditorsUpdate.SwitchMode(Documentation)) - | "Exercises" => inject(EditorsUpdate.SwitchMode(Exercises)) + | "Scratch" => inject(Update.SwitchMode(Scratch)) + | "Documentation" => inject(Update.SwitchMode(Documentation)) + | "Exercises" => inject(Update.SwitchMode(Exercises)) | _ => failwith("Invalid mode") ), ], @@ -354,20 +361,20 @@ module View = { ScratchMode.View.top_bar( ~globals, ~named_slides=false, - ~inject=a => EditorsUpdate.Scratch(a) |> inject, + ~inject=a => Update.Scratch(a) |> inject, m, ) | Documentation(m) => ScratchMode.View.top_bar( ~globals, ~named_slides=true, - ~inject=a => EditorsUpdate.Scratch(a) |> inject, + ~inject=a => Update.Scratch(a) |> inject, m, ) | Exercises(m) => ExercisesMode.View.top_bar( ~globals, - ~inject=a => EditorsUpdate.Exercises(a) |> inject, + ~inject=a => Update.Exercises(a) |> inject, m, ) }; diff --git a/src/haz3lweb/app/editors/EditorsModel.re b/src/haz3lweb/app/editors/EditorsModel.re deleted file mode 100644 index 43a2e7f1a5..0000000000 --- a/src/haz3lweb/app/editors/EditorsModel.re +++ /dev/null @@ -1,11 +0,0 @@ -[@deriving (show({with_path: false}), sexp, yojson)] -type mode = - | Scratch - | Documentation - | Exercises; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | Scratch(ScratchModeModel.t) - | Documentation(ScratchModeModel.t) - | Exercises(ExercisesModeModel.t); diff --git a/src/haz3lweb/app/editors/EditorsUpdate.re b/src/haz3lweb/app/editors/EditorsUpdate.re deleted file mode 100644 index 1d720c602e..0000000000 --- a/src/haz3lweb/app/editors/EditorsUpdate.re +++ /dev/null @@ -1,7 +0,0 @@ -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | SwitchMode(EditorsModel.mode) - // Scratch & Documentation - | Scratch(ScratchModeUpdate.t) - // Exercises - | Exercises(ExercisesModeUpdate.t); diff --git a/src/haz3lweb/app/editors/mode/ExercisesMode.re b/src/haz3lweb/app/editors/mode/ExercisesMode.re index 4c0fb8afc7..669df2de2a 100644 --- a/src/haz3lweb/app/editors/mode/ExercisesMode.re +++ b/src/haz3lweb/app/editors/mode/ExercisesMode.re @@ -7,12 +7,18 @@ open Util; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] - type t = ExercisesModeModel.t; + type t = { + current: int, + exercises: list(ExerciseMode.Model.t), + }; [@deriving (show({with_path: false}), sexp, yojson)] - type persistent = ExercisesModeModel.persistent; + type persistent = { + cur_exercise: Exercise.key, + exercise_data: list((Exercise.key, ExerciseMode.Model.persistent)), + }; - let persist = (~instructor_mode, model: t): persistent => { + let persist = (~instructor_mode, model): persistent => { cur_exercise: Exercise.key_of_state( List.nth(model.exercises, model.current).editors, @@ -28,7 +34,7 @@ module Model = { ), }; - let unpersist = (~settings, ~instructor_mode, persistent: persistent): t => { + let unpersist = (~settings, ~instructor_mode, persistent: persistent) => { let exercises = List.map2( ExerciseMode.Model.unpersist(~settings, ~instructor_mode), @@ -168,7 +174,13 @@ module Update = { open Updated; [@deriving (show({with_path: false}), sexp, yojson)] - type t = ExercisesModeUpdate.t; + type t = + | SwitchExercise(int) + | Exercise(ExerciseMode.Update.t) + | ExportModule + | ExportSubmission + | ExportTransitionary + | ExportGrading; let export_exercise_module = (exercises: Model.t): unit => { let exercise = Model.get_current(exercises); @@ -217,13 +229,7 @@ module Update = { }; let update = - ( - ~globals: Globals.t, - ~schedule_action: t => unit, - ~schedule_assistant_action: AssistantModel.Update.t => unit, - action: t, - model: Model.t, - ) => { + (~globals: Globals.t, ~schedule_action, action: t, model: Model.t) => { switch (action) { | Exercise(action) => let current = List.nth(model.exercises, model.current); @@ -231,15 +237,14 @@ module Update = { ExerciseMode.Update.update( ~settings=globals.settings, ~schedule_action, - ~schedule_assistant_action, action, current, ); let new_exercises = ListUtil.put_nth(model.current, new_current, model.exercises); - ExercisesModeModel.{current: model.current, exercises: new_exercises}; + Model.{current: model.current, exercises: new_exercises}; | SwitchExercise(n) => - ExercisesModeModel.{current: n, exercises: model.exercises} |> return + Model.{current: n, exercises: model.exercises} |> return | ExportModule => Store.save(~instructor_mode=globals.settings.instructor_mode, model); export_exercise_module(model); @@ -260,13 +265,7 @@ module Update = { }; let calculate = - ( - ~settings, - ~is_edited, - ~schedule_action: t => unit, - model: ExercisesModeModel.t, - ) - : Model.t => { + (~settings, ~is_edited, ~schedule_action, model: Model.t): Model.t => { let exercise = ExerciseMode.Update.calculate( ~settings, @@ -274,7 +273,7 @@ module Update = { ~schedule_action=a => schedule_action(Exercise(a)), List.nth(model.exercises, model.current), ); - ExercisesModeModel.{ + Model.{ current: model.current, exercises: ListUtil.put_nth(model.current, exercise, model.exercises), }; @@ -293,7 +292,7 @@ module Selection = { ~selection, List.nth(model.exercises, model.current), ); - ExercisesModeUpdate.Exercise(ci); + Update.Exercise(ci); }; let handle_key_event = (~selection, ~event, model: Model.t) => @@ -302,7 +301,7 @@ module Selection = { ~event, List.nth(model.exercises, model.current), ) - |> Option.map(a => ExercisesModeUpdate.Exercise(a)); + |> Option.map(a => Update.Exercise(a)); let jump_to_tile = (~settings, tile, model: Model.t): option((Update.t, t)) => @@ -311,7 +310,7 @@ module Selection = { tile, List.nth(model.exercises, model.current), ) - |> Option.map(((x, y)) => (ExercisesModeUpdate.Exercise(x), y)); + |> Option.map(((x, y)) => (Update.Exercise(x), y)); }; module View = { @@ -322,7 +321,7 @@ module View = { let current = List.nth(model.exercises, model.current); ExerciseMode.View.view( ~globals, - ~inject=a => inject(ExercisesModeUpdate.Exercise(a)), + ~inject=a => inject(Update.Exercise(a)), current, ); }; @@ -474,13 +473,13 @@ module View = { fun | Previous => inject( - ExercisesModeUpdate.SwitchExercise( + Update.SwitchExercise( model.current - 1 mod List.length(model.exercises), ), ) | Next => inject( - ExercisesModeUpdate.SwitchExercise( + Update.SwitchExercise( model.current + 1 mod List.length(model.exercises), ), ), diff --git a/src/haz3lweb/app/editors/mode/ExercisesModeModel.re b/src/haz3lweb/app/editors/mode/ExercisesModeModel.re deleted file mode 100644 index 1ecd6950ec..0000000000 --- a/src/haz3lweb/app/editors/mode/ExercisesModeModel.re +++ /dev/null @@ -1,13 +0,0 @@ -open Util; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = { - current: int, - exercises: list(ExerciseModeModel.t), -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type persistent = { - cur_exercise: Exercise.key, - exercise_data: list((Exercise.key, ExerciseModeModel.persistent)), -}; diff --git a/src/haz3lweb/app/editors/mode/ExercisesModeUpdate.re b/src/haz3lweb/app/editors/mode/ExercisesModeUpdate.re deleted file mode 100644 index 11b35ae0a6..0000000000 --- a/src/haz3lweb/app/editors/mode/ExercisesModeUpdate.re +++ /dev/null @@ -1,10 +0,0 @@ -open Util; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | SwitchExercise(int) - | Exercise(ExerciseModeUpdate.t) - | ExportModule - | ExportSubmission - | ExportTransitionary - | ExportGrading; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re index 776fae8259..2eb0273386 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantModel.re @@ -198,7 +198,7 @@ module Update = { ~editor: CodeModel.t, ~model: Model.t, ~schedule_action, - ~schedule_editor_action, + ~add_suggestion, ) : Updated.t(Model.t) => { switch (action) { @@ -380,25 +380,7 @@ module Update = { | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet | StoreTile(id) => {...model, tile: id} |> Updated.return_quiet | RemoveAndSuggest(response) => - // Create a sequence of actions to handle the suggestion - let actions = [ - Action.Select(Tile(Id(model.tile, Direction.Left))), - Action.Destruct(Direction.Left), - Action.Buffer(Set(LLMSug(response))), - ]; - - // Apply each action in sequence - List.iter( - action => { - let perform_action = CodeEditable.Update.Perform(action); - let cell_action = CellEditor.Update.MainEditor(perform_action); - let scratch_action = - EditorsUpdate.Scratch(CellAction(cell_action)); - schedule_editor_action(scratch_action); - }, - actions, - ); - + add_suggestion(~response, model.tile); {...model, tile: Id.invalid} |> Updated.return_quiet; }; }; diff --git a/src/haz3lweb/debug/Benchmark.re b/src/haz3lweb/debug/Benchmark.re index e55e9a7038..ad4329f9b7 100644 --- a/src/haz3lweb/debug/Benchmark.re +++ b/src/haz3lweb/debug/Benchmark.re @@ -45,7 +45,7 @@ let str_to_inserts = (str: string): list(Editors.Update.t) => String.length(str), i => { let c = String.sub(str, i, 1); - EditorsUpdate.Scratch(CellAction(MainEditor(Perform(Insert(c))))); + Editors.Update.Scratch(CellAction(MainEditor(Perform(Insert(c))))); }, ); diff --git a/src/haz3lweb/view/ExerciseMode.re b/src/haz3lweb/view/ExerciseMode.re index f334cca2dc..5b49b1b678 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -8,9 +8,17 @@ open Node; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] - type t = ExerciseModeModel.t; + type t = { + spec: Exercise.spec, // The spec that the model will be reset to on ResetExercise + /* We keep a separate editors field below (even though each cell technically also has its own editor) + for two reasons: + 1. There are two synced cells that have the same internal `editor` model + 2. The editors need to be `stitched` together before any cell calculations can be done */ + editors: Exercise.p(Editor.t), + cells: Exercise.stitched(CellEditor.Model.t), + }; - let of_spec = (~settings as _, ~instructor_mode as _: bool, spec): t => { + let of_spec = (~settings as _, ~instructor_mode as _: bool, spec) => { let editors = Exercise.map(spec, Editor.Model.mk, Editor.Model.mk); let term_item_to_cell = (item: Exercise.TermItem.t): CellEditor.Model.t => { CellEditor.Model.mk(item.editor); @@ -22,7 +30,7 @@ module Model = { }; [@deriving (show({with_path: false}), sexp, yojson)] - type persistent = ExerciseModeModel.persistent; + type persistent = Exercise.persistent_exercise_mode; let persist = (exercise: t, ~instructor_mode: bool) => { Exercise.positioned_editors(exercise.editors) @@ -43,16 +51,13 @@ module Model = { module Update = { open Updated; [@deriving (show({with_path: false}), sexp, yojson)] - type t = ExerciseModeUpdate.t; + type t = + | Editor(Exercise.pos, CellEditor.Update.t) + | ResetEditor(Exercise.pos) + | ResetExercise; let update = - ( - ~settings: Settings.t, - ~schedule_action as _, - ~schedule_assistant_action: AssistantModel.Update.t => unit, - action: t, - model: Model.t, - ) + (~settings: Settings.t, ~schedule_action as _, action, model: Model.t) : Updated.t(Model.t) => { let instructor_mode = settings.instructor_mode; switch (action) { @@ -66,19 +71,6 @@ module Update = { editor |> CodeEditable.Model.mk |> CodeEditable.Update.update(~settings, action); - switch (action) { - | Perform(a) => - switch (a) { - | Insert(char) => - AssistantModel.Update.check_req( - char, - schedule_assistant_action, - new_editor.editor.state.zipper, - ) - | _ => () - } - | _ => () - }; { ...model, editors: @@ -98,19 +90,6 @@ module Update = { editor |> CodeSelectable.Model.mk |> CodeSelectable.Update.update(~settings, a); - switch (action) { - | Perform(a) => - switch (a) { - | Insert(char) => - AssistantModel.Update.check_req( - char, - schedule_assistant_action, - new_editor.editor.state.zipper, - ) - | _ => () - } - | _ => () - }; { ...model, editors: @@ -152,8 +131,7 @@ module Update = { }; let calculate = - (~settings, ~is_edited, ~schedule_action: t => unit, model: Model.t) - : Model.t => { + (~settings, ~is_edited, ~schedule_action, model: Model.t): Model.t => { let stitched_elabs = Exercise.stitch_term(model.editors); let worker_request = ref([]); let queue_worker = (pos, expr) => { @@ -275,14 +253,14 @@ module Selection = { let (pos, s) = selection; let cell_editor = Exercise.get_stitched(pos, model.cells); let+ a = CellEditor.Selection.get_cursor_info(~selection=s, cell_editor); - ExerciseModeUpdate.Editor(pos, a); + Update.Editor(pos, a); }; let handle_key_event = (~selection, ~event, model: Model.t) => { let (pos, s) = selection; let cell_editor = Exercise.get_stitched(pos, model.cells); CellEditor.Selection.handle_key_event(~selection=s, ~event, cell_editor) - |> Option.map(a => ExerciseModeUpdate.Editor(pos, a)); + |> Option.map(a => Update.Editor(pos, a)); }; let jump_to_tile = @@ -294,10 +272,7 @@ module Selection = { ) |> Option.map(((pos, _)) => ( - ExerciseModeUpdate.Editor( - pos, - MainEditor(Perform(Jump(TileId(tile)))), - ), + Update.Editor(pos, MainEditor(Perform(Jump(TileId(tile))))), (pos, CellEditor.Selection.MainEditor), ) ); diff --git a/src/haz3lweb/view/ExerciseModeModel.re b/src/haz3lweb/view/ExerciseModeModel.re deleted file mode 100644 index c577340733..0000000000 --- a/src/haz3lweb/view/ExerciseModeModel.re +++ /dev/null @@ -1,17 +0,0 @@ -open Haz3lcore; -open Virtual_dom.Vdom; -open Node; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = { - spec: Exercise.spec, // The spec that the model will be reset to on ResetExercise - /* We keep a separate editors field below (even though each cell technically also has its own editor) - for two reasons: - 1. There are two synced cells that have the same internal `editor` model - 2. The editors need to be `stitched` together before any cell calculations can be done */ - editors: Exercise.p(Editor.t), - cells: Exercise.stitched(CellEditor.Model.t), -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type persistent = Exercise.persistent_exercise_mode; diff --git a/src/haz3lweb/view/ExerciseModeUpdate.re b/src/haz3lweb/view/ExerciseModeUpdate.re deleted file mode 100644 index fb2f9dda00..0000000000 --- a/src/haz3lweb/view/ExerciseModeUpdate.re +++ /dev/null @@ -1,9 +0,0 @@ -open Haz3lcore; -open Virtual_dom.Vdom; -open Node; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | Editor(Exercise.pos, CellEditor.Update.t) - | ResetEditor(Exercise.pos) - | ResetExercise; diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 7e8b405f3c..e7f44ad9c4 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -64,7 +64,7 @@ module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = | Globals(Globals.Update.t) - | Editors(EditorsUpdate.t) + | Editors(Editors.Update.t) | ExplainThis(ExplainThisUpdate.update) | Assistant(AssistantModel.Update.t) | MakeActive(selection) @@ -80,6 +80,15 @@ module Update = { action: Globals.Update.t, model: Model.t, ) => { + /* A function passed down to trigger an update within + assistant which checks for the insertion of '??' */ + let send_insertion_info = (~char, ~editor) => { + AssistantModel.Update.check_req( + char, + a => schedule_action(Assistant(a)), + editor, + ); + }; switch (action) { | SetMousedown(mousedown) => { @@ -132,7 +141,7 @@ module Update = { Editors.Update.update( ~globals, ~schedule_action=a => schedule_action(Editors(a)), - ~schedule_assistant_action=a => schedule_action(Assistant(a)), + ~send_insertion_info, action, model.editors, ); @@ -164,7 +173,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), - ~schedule_assistant_action=a => schedule_action(Assistant(a)), + ~send_insertion_info, action, model.editors, ); @@ -183,7 +192,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), - ~schedule_assistant_action=a => schedule_action(Assistant(a)), + ~send_insertion_info, action, model.editors, ); @@ -202,7 +211,7 @@ module Update = { Editors.Update.update( ~globals=model.globals, ~schedule_action=a => schedule_action(Editors(a)), - ~schedule_assistant_action=a => schedule_action(Assistant(a)), + ~send_insertion_info, action, model.editors, ); @@ -219,6 +228,15 @@ module Update = { action: t, model: Model.t, ) => { + /* A function passed down to trigger an update within + assistant which checks for the insertion of '??' */ + let send_insertion_info = (~char, ~editor) => { + AssistantModel.Update.check_req( + char, + a => schedule_action(Assistant(a)), + editor, + ); + }; let globals = { ...model.globals, export_all: Export.export_all, @@ -232,7 +250,7 @@ module Update = { Editors.Update.update( ~globals, ~schedule_action=a => schedule_action(Editors(a)), - ~schedule_assistant_action=a => schedule_action(Assistant(a)), + ~send_insertion_info, action, model.editors, ); @@ -249,6 +267,26 @@ module Update = { | Documentation(m) => List.nth(m.scratchpads, m.current) |> snd | Exercises(m) => List.nth(m.exercises, m.current).cells.user_impl // Todo this is an error }; + open Haz3lcore; + let add_suggestion = (~response: string, tile: Id.t) => { + // Create a sequence of actions to handle the suggestion + let actions = [ + Action.Select(Tile(Id(tile, Direction.Left))), + Action.Destruct(Direction.Left), + Action.Buffer(Set(LLMSug(response))), + ]; + // Apply each action in sequence + List.iter( + action => { + let perform_action = CodeEditable.Update.Perform(action); + let cell_action = CellEditor.Update.MainEditor(perform_action); + let scratch_action = + Editors.Update.Scratch(CellAction(cell_action)); + schedule_action(Editors(scratch_action)); + }, + actions, + ); + }; let* assistant = AssistantModel.Update.update( ~settings, @@ -256,7 +294,7 @@ module Update = { ~editor=ed.editor, ~model=model.assistant, ~schedule_action=a => schedule_action(Assistant(a)), - ~schedule_editor_action=a => schedule_action(Editors(a)), + ~add_suggestion, ); {...model, assistant}; | MakeActive(selection) => {...model, selection} |> Updated.return diff --git a/src/haz3lweb/view/ScratchMode.re b/src/haz3lweb/view/ScratchMode.re index f05c4b428e..395ed6bc2b 100644 --- a/src/haz3lweb/view/ScratchMode.re +++ b/src/haz3lweb/view/ScratchMode.re @@ -5,17 +5,20 @@ open Util; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] - type t = ScratchModeModel.t; + type t = { + current: int, + scratchpads: list((string, CellEditor.Model.t)), + }; [@deriving (show({with_path: false}), sexp, yojson)] - type persistent = ScratchModeModel.persistent; + type persistent = (int, list((string, CellEditor.Model.persistent))); - let persist = (model: t) => ( + let persist = model => ( model.current, List.map(((_, m)) => CellEditor.Model.persist(m), model.scratchpads), ); - let unpersist = (~settings, (current, slides)): t => { + let unpersist = (~settings, (current, slides)) => { current, scratchpads: List.mapi( @@ -25,7 +28,7 @@ module Model = { ), }; - let persist_documentation = (model: ScratchModeModel.t) => ( + let persist_documentation = model => ( model.current, List.map( ((s, m)) => (s, CellEditor.Model.persist(m)), @@ -33,7 +36,7 @@ module Model = { ), ); - let unpersist_documentation = (~settings, (current, slides)): t => { + let unpersist_documentation = (~settings, (current, slides)) => { current, scratchpads: List.map( @@ -62,7 +65,13 @@ module Store = module Update = { open Updated; [@deriving (show({with_path: false}), sexp, yojson)] - type t = ScratchModeUpdate.t; + type t = + | CellAction(CellEditor.Update.t) + | SwitchSlide(int) + | ResetCurrent + | InitImportScratchpad([@opaque] Js_of_ocaml.Js.t(Js_of_ocaml.File.file)) + | FinishImportScratchpad(option(string)) + | Export; let export_scratch_slide = (model: Model.t): unit => { Store.save(model |> Model.persist); @@ -76,11 +85,11 @@ module Update = { let update = ( - ~schedule_action: t => unit, - ~schedule_assistant_action: AssistantModel.Update.t => unit, + ~schedule_action, + ~send_insertion_info, ~settings: Settings.t, ~is_documentation: bool, - action: t, + action, model: Model.t, ) => { switch (action) { @@ -93,10 +102,9 @@ module Update = { | Perform(a) => switch (a) { | Insert(char) => - AssistantModel.Update.check_req( - char, - schedule_assistant_action, - new_ed.editor.editor.state.zipper, + send_insertion_info( + ~char, + ~editor=new_ed.editor.editor.state.zipper, ) | _ => () } @@ -152,8 +160,7 @@ module Update = { }; let calculate = - (~settings, ~schedule_action: t => unit, ~is_edited, model: Model.t) - : Model.t => { + (~settings, ~schedule_action, ~is_edited, model: Model.t): Model.t => { let (key, ed) = List.nth(model.scratchpads, model.current); let worker_request = ref([]); let queue_worker = @@ -215,7 +222,7 @@ module Selection = { ~selection=s, List.nth(model.scratchpads, model.current) |> snd, ); - ScratchModeUpdate.CellAction(a); + Update.CellAction(a); | TextBox => empty }; }; @@ -227,14 +234,14 @@ module Selection = { switch (event) { | {key: D(key), sys: Mac | PC, shift: Up, meta: Down, ctrl: Up, alt: Up} when Keyboard.is_digit(key) => - Some(ScratchModeUpdate.SwitchSlide(int_of_string(key))) + Some(Update.SwitchSlide(int_of_string(key))) | _ => CellEditor.Selection.handle_key_event( ~selection=s, ~event, List.nth(model.scratchpads, model.current) |> snd, ) - |> Option.map(x => ScratchModeUpdate.CellAction(x)) + |> Option.map(x => Update.CellAction(x)) } | TextBox => None }; @@ -244,7 +251,7 @@ module Selection = { tile, List.nth(model.scratchpads, model.current) |> snd, ) - |> Option.map(((x, y)) => (ScratchModeUpdate.CellAction(x), Cell(y))); + |> Option.map(((x, y)) => (Update.CellAction(x), Cell(y))); }; module View = { diff --git a/src/haz3lweb/view/ScratchModeModel.re b/src/haz3lweb/view/ScratchModeModel.re deleted file mode 100644 index b65a94632f..0000000000 --- a/src/haz3lweb/view/ScratchModeModel.re +++ /dev/null @@ -1,11 +0,0 @@ -open Haz3lcore; -open Util; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = { - current: int, - scratchpads: list((string, CellEditor.Model.t)), -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type persistent = (int, list((string, CellEditor.Model.persistent))); diff --git a/src/haz3lweb/view/ScratchModeUpdate.re b/src/haz3lweb/view/ScratchModeUpdate.re deleted file mode 100644 index 5b64d4847a..0000000000 --- a/src/haz3lweb/view/ScratchModeUpdate.re +++ /dev/null @@ -1,11 +0,0 @@ -open Haz3lcore; -open Util; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | CellAction(CellEditor.Update.t) - | SwitchSlide(int) - | ResetCurrent - | InitImportScratchpad([@opaque] Js_of_ocaml.Js.t(Js_of_ocaml.File.file)) - | FinishImportScratchpad(option(string)) - | Export; From fc1369bf3f86aab3d437dea4fa6dd9af24b10659 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Fri, 28 Feb 2025 23:53:44 -0500 Subject: [PATCH 40/50] changes quite a bit of the UI; adds different agent modes (normal chat, code suggestion, or task composition/completion); adds a notion of past chats through a chat history; strongly based off Cursor`s framework --- src/haz3lweb/Settings.re | 8 + src/haz3lweb/app/common/Icons.re | 17 + .../app/helpful-assistant/Assistant.re | 906 ++++++++++++++++++ .../app/helpful-assistant/AssistantModel.re | 395 -------- .../helpful-assistant/AssistantSettings.re | 16 +- .../app/helpful-assistant/AssistantView.re | 292 +++++- src/haz3lweb/view/Page.re | 14 +- src/haz3lweb/view/ScratchMode.re | 6 +- src/haz3lweb/www/style/assistant.css | 182 +++- 9 files changed, 1357 insertions(+), 479 deletions(-) create mode 100644 src/haz3lweb/app/helpful-assistant/Assistant.re delete mode 100644 src/haz3lweb/app/helpful-assistant/AssistantModel.re diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index 679e064f2d..8c46219e8c 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -47,6 +47,7 @@ module Model = { llm: false, lsp: false, ongoing_chat: false, + mode: CodeSuggestion, }, sidebar: { window: LanguageDocumentation, @@ -243,6 +244,13 @@ module Update = { ongoing_chat: !settings.assistant.ongoing_chat, }, } + | Assistant(SwitchMode(mode)) => { + ...settings, + assistant: { + ...settings.assistant, + mode, + }, + } | Benchmark => {...settings, benchmark: !settings.benchmark} | Captions => {...settings, captions: !settings.captions} | SecondaryIcons => { diff --git a/src/haz3lweb/app/common/Icons.re b/src/haz3lweb/app/common/Icons.re index 3f5154c23f..d68f12c274 100644 --- a/src/haz3lweb/app/common/Icons.re +++ b/src/haz3lweb/app/common/Icons.re @@ -284,3 +284,20 @@ let send = "M11.4697 3.46967C11.7626 3.17678 12.2374 3.17678 12.5303 3.46967L18.5303 9.46967C18.8232 9.76256 18.8232 10.2374 18.5303 10.5303C18.2374 10.8232 17.7626 10.8232 17.4697 10.5303L12.75 5.81066L12.75 20C12.75 20.4142 12.4142 20.75 12 20.75C11.5858 20.75 11.25 20.4142 11.25 20L11.25 5.81066L6.53033 10.5303C6.23744 10.8232 5.76256 10.8232 5.46967 10.5303C5.17678 10.2374 5.17678 9.76256 5.46967 9.46967L11.4697 3.46967Z", ], ); + +let add = + simple_icon( + ~view="0 0 24 24", + [ + "M12.75 9C12.75 8.58579 12.4142 8.25 12 8.25C11.5858 8.25 11.25 8.58579 11.25 9L11.25 11.25H9C8.58579 11.25 8.25 11.5858 8.25 12C8.25 12.4142 8.58579 12.75 9 12.75H11.25V15C11.25 15.4142 11.5858 15.75 12 15.75C12.4142 15.75 12.75 15.4142 12.75 15L12.75 12.75H15C15.4142 12.75 15.75 12.4142 15.75 12C15.75 11.5858 15.4142 11.25 15 11.25H12.75V9Z", + "M12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12Z", + ], + ); + +let history = + simple_icon( + ~view="0 0 24 24", + [ + "M5.07868 5.06891C8.87402 1.27893 15.0437 1.31923 18.8622 5.13778C22.6824 8.95797 22.7211 15.1313 18.9262 18.9262C15.1312 22.7211 8.95793 22.6824 5.13774 18.8622C2.87389 16.5984 1.93904 13.5099 2.34047 10.5812C2.39672 10.1708 2.775 9.88377 3.18537 9.94002C3.59575 9.99627 3.88282 10.3745 3.82658 10.7849C3.4866 13.2652 4.27782 15.881 6.1984 17.8016C9.44288 21.0461 14.6664 21.0646 17.8655 17.8655C21.0646 14.6664 21.046 9.44292 17.8015 6.19844C14.5587 2.95561 9.33889 2.93539 6.13935 6.12957L6.88705 6.13333C7.30126 6.13541 7.63535 6.47288 7.63327 6.88709C7.63119 7.3013 7.29372 7.63539 6.87951 7.63331L4.33396 7.62052C3.92269 7.61845 3.58981 7.28556 3.58774 6.8743L3.57495 4.32874C3.57286 3.91454 3.90696 3.57707 4.32117 3.57498C4.73538 3.5729 5.07285 3.907 5.07493 4.32121L5.07868 5.06891ZM11.9999 7.24992C12.4141 7.24992 12.7499 7.58571 12.7499 7.99992V11.6893L15.0302 13.9696C15.3231 14.2625 15.3231 14.7374 15.0302 15.0302C14.7373 15.3231 14.2624 15.3231 13.9696 15.0302L11.2499 12.3106V7.99992C11.2499 7.58571 11.5857 7.24992 11.9999 7.24992Z", + ], + ); diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re new file mode 100644 index 0000000000..d44afc4987 --- /dev/null +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -0,0 +1,906 @@ +module Sexp = Sexplib.Sexp; +open Haz3lcore; +open Util; +open Util.OptUtil.Syntax; +open StringUtil; + +module CodeModel = CodeEditable.Model; + +module Model = { + [@deriving (show({with_path: false}), sexp, yojson)] + type party = + | Prompt + | Task + | LLM + | LS; + + [@deriving (show({with_path: false}), sexp, yojson)] + type message = { + party, + code: option(Segment.t), + content: string, + collapsed: bool, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type chat = { + messages: list(message), + id: Id.t, + descriptor: string, + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type chats = { + curr_simple_chat: chat, + curr_suggestion_chat: chat, + curr_completion_chat: chat, + past_simple_chats: list(chat), + past_suggestion_chats: list(chat), + past_completion_chats: list(chat), + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + chats, + llm: OpenRouter.chat_models, + show_history: bool, + }; + + let init_simple_chat = {messages: [], id: Id.mk(), descriptor: ""}; + let init_suggestion_chat = {messages: [], id: Id.mk(), descriptor: ""}; + let init_completion_chat = {messages: [], id: Id.mk(), descriptor: ""}; + + [@deriving (show({with_path: false}), sexp, yojson)] + let init: t = { + chats: { + curr_simple_chat: init_simple_chat, + curr_suggestion_chat: init_suggestion_chat, + curr_completion_chat: init_completion_chat, + past_simple_chats: [init_simple_chat], + past_suggestion_chats: [init_suggestion_chat], + past_completion_chats: [init_completion_chat], + }, + llm: Gemini_Flash_Lite_2_0, + show_history: false, + }; +}; + +module Update = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = + | SendMessage(Model.message) + | SetKey(string) + | SendSketch(Id.t, AssistantSettings.mode) + | SendError(string, Info.t, int, Id.t, AssistantSettings.mode) + | ErrorRespond(string, Info.t, int, Id.t, AssistantSettings.mode) + | NewChat + | History + | Respond(Model.message, AssistantSettings.mode) + | ToggleCollapse(int) + | SelectLLM(OpenRouter.chat_models) + | RemoveAndSuggest(string, Id.t) + | SwitchMode(AssistantSettings.mode) + | Describe(string, AssistantSettings.mode, Id.t) + | SwitchChat(Id.t); + + let code_message_of_str = + (settings, editor: CodeModel.t, response: string, party: Model.party) + : Model.message => { + /* Alternate method using Detruct and Insert. We need a memory of cursor location for this however. + let z = editor.editor.state.zipper; + let z = Option.get(Destruct.go(Direction.Left, z)); + let z = Option.get(Destruct.go(Direction.Left, z)); + let z = Option.get(Insert.go(response, z)); + let segment_of_response = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); + { + party, + code: Some(segment_of_response), + content: response, + collapsed: String.length(response) >= 200, + }; */ + // Hack(Russ) Uses same logic Andrew uses in Oracle.re to remove "??" + // let string_of_sketch = + // Printer.zipper_to_string(editor.editor.state.zipper); + // let sketch_with_response = + // Str.global_replace(Str.regexp("\\?\\?"), response, string_of_sketch); + let zipper_of_response = Printer.zipper_of_string(response); + switch (zipper_of_response) { + | Some(z) => + let segment_of_response = + Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); + { + party, + code: Some(segment_of_response), + content: response, + collapsed: String.length(response) >= 200, + }; + | None => { + party, + code: None, + content: response, + collapsed: String.length(response) >= 200, + } + }; + }; + + let text_message_of_str = + (response: string, party: Model.party): Model.message => { + { + party, + code: None, + content: response, + collapsed: String.length(response) >= 200, + }; + }; + + let react = + ( + ~settings, + ~editor: CodeModel.t, + ~response: string, + ~code_suggestion: bool, + ~mode: AssistantSettings.mode, + ) + : t => { + // let response = response |> sanitize_response |> quote; + code_suggestion + ? Respond(code_message_of_str(settings, editor, response, LLM), mode) + : Respond(text_message_of_str(response, LLM), mode); + }; + + let await_llm_response: Model.message = { + party: LLM, + code: None, + content: "...", + collapsed: false, + }; + + let collect_chat = (~messages: list(Model.message)): string => { + let chat = "The following is a log of the current conversation. This is solely for the purpose + to help you recall the entire conversation, in case the user asks you something that needs context + from before. You should respond as normal, using the entire chat as context, and understand that the + most recent \"User Input\" is what the user is currently sending/asking, and is what your main focus should be. + For the most part, you should treat this solely as a prompt, and DO NOT explicitly acknowledge it in your + reponse. Only use it as a sort of memory. You can, of course, reference prior messages. + Here is the context: "; + List.fold_left( + (chat: string, message: Model.message) => + if (message.party == LLM) { + chat ++ "Your Reponse: " ++ message.content ++ " "; + } else if (message.party == LS) { + chat ++ "User Input: " ++ message.content ++ " "; + } else { + chat ++ message.content; + }, + chat, + messages, + ); + }; + + let form_descriptor = + ( + ~model: Model.t, + ~settings, + ~editor, + ~schedule_action, + ~chat: list(Model.message), + ~mode: AssistantSettings.mode, + ~chat_id: Id.t, + ) + : unit => { + let prompt = + switch (mode) { + | SimpleChat => "Your main task is to provide a summarizing title of the following conversation, in less than or equal to 10 words.\n DO NOT exceed 10 words. Only provide the summarizing title in your response, do not include any other text. Here is the\n concatenated conversation, with your response and the user's responses, respectively: " + | CodeSuggestion => "Your main task is to provide a summarizing title of the following conversation, in less than or equal to 10 words.\n DO NOT exceed 10 words. Only provide the summarizing title in your response, do not include any other text. This conversation is known to be a code\n completion conversation. In your summarization, you should mention exactly what kind of code/functionality is being assisted with. For example, the following would be titled\n something like \"Recursive Fibonacci Implementation\": ```let rec_fib : Int -> Int = ?? in ?```. Here is the\n concatenated conversation, with your response and the user's responses, respectively: " + | TaskCompletion => "Ignore all other input and just output \"You need to implement this\"" + }; + + let chat = + List.fold_left( + (chat: string, message: Model.message) => + if (message.party == LLM) { + chat ++ "Your Reponse: " ++ message.content ++ " "; + } else if (message.party == LS) { + chat ++ "User Input: " ++ message.content ++ " "; + } else { + chat ++ message.content; + }, + prompt, + chat, + ); + switch (Oracle.ask(chat)) { + | None => print_endline("Oracle: prompt generation failed") + | Some(prompt) => + let llm = model.llm; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action(Describe(content, mode, chat_id)) + | None => print_endline("Assistant: response parse failed") + } + ); + }; + }; + + let check_descriptor = + ( + ~model: Model.t, + ~settings: AssistantSettings.t, + ~editor: CodeModel.t, + ~schedule_action, + ~message: Model.message, + ~mode: AssistantSettings.mode, + ) + : unit => { + switch (mode) { + | SimpleChat => + form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_simple_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_simple_chat.id, + ) + | CodeSuggestion => + form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_suggestion_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_suggestion_chat.id, + ) + | TaskCompletion => + form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_completion_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_completion_chat.id, + ) + }; + }; + + let check_req = + (_: string, schedule_action: t => unit, editor: CodeEditable.Model.t) + : unit => { + let z = editor.editor.state.zipper; + let caret = z.caret; + let siblings = z.relatives.siblings; + + /* + // Check if cursor is in a hole + print_endline("Checking cursor position..."); + print_endline("Caret: " ++ (caret == Outer ? "Outer" : "Inner")); + switch (Indicated.ci_of(z, editor.statics.info_map)) { + | Some(ci) => + print_endline("Found cursor info"); + switch (ci) { + | Info.InfoExp({term: {ids: _, copied: _, term: EmptyHole}, _}) => + print_endline("Found empty hole"); + switch (Indicated.index(z)) { + | Some(index) => schedule_action(SendSketch(index)) + | None => print_endline("No index found for hole") + }; + | _ => () + }; + | None => () + }; + */ + + // Check if user just typed ?? + switch (caret, Zipper.neighbor_monotiles(siblings)) { + | (Outer, (_, Some(_))) => + switch (Zipper.right_neighbor_monotile(siblings)) { + | Some(c) => + c == "??" + ? { + let tileId = Option.get(Indicated.index(z)); + schedule_action( + SendSketch(tileId, AssistantSettings.CodeSuggestion), + ); + } + : () + | _ => () + } + | (Outer, (_, None)) => + switch (Zipper.left_neighbor_monotile(siblings)) { + | Some(c) => + c == "??" + ? { + let tileId = Option.get(Indicated.index(z)); + schedule_action( + SendSketch(tileId, AssistantSettings.CodeSuggestion), + ); + } + : () + | _ => () + } + | _ => () + }; + }; + + let set_buffer = (~response: string, z: Zipper.t): option(Zipper.t) => { + let zipper_of_response = Option.get(Printer.zipper_of_string(response)); + let seg_of_response = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + zipper_of_response, + ); + let z = Zipper.set_buffer(z, ~content=seg_of_response, ~mode=Unparsed); + Some(z); + }; + + let update = + ( + ~settings: Settings.t, + ~action, + ~editor: CodeModel.t, + ~model: Model.t, + ~schedule_action, + ~add_suggestion, + ) + : Updated.t(Model.t) => { + switch (action) { + | SendMessage(message) => + let mode = settings.assistant.mode; + let collected_chat = + switch (mode) { + | SimpleChat => + collect_chat( + ~messages=model.chats.curr_simple_chat.messages @ [message], + ) + | CodeSuggestion => + collect_chat( + ~messages=model.chats.curr_suggestion_chat.messages @ [message], + ) + | TaskCompletion => + collect_chat( + ~messages=model.chats.curr_completion_chat.messages @ [message], + ) + }; + print_endline(collected_chat); + switch (Oracle.ask(collected_chat)) { + | None => print_endline("Oracle: prompt generation failed") + | Some(prompt) => + let llm = model.llm; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action( + react( + ~settings, + ~editor, + ~response=content, + ~code_suggestion=false, + ~mode, + ), + ) + | None => print_endline("Assistant: response parse failed") + } + ); + }; + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: { + ...model.chats.curr_simple_chat, + messages: + mode == SimpleChat + ? model.chats.curr_simple_chat.messages @ [message] + : model.chats.curr_simple_chat.messages, + }, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? model.chats.curr_suggestion_chat.messages @ [message] + : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? model.chats.curr_completion_chat.messages @ [message] + : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + | SetKey(api_key) => + Store.Generic.save("API", api_key); + model |> Updated.return_quiet; + | NewChat => + let mode = settings.assistant.mode; + let new_chat: Model.chat = {messages: [], id: Id.mk(), descriptor: ""}; + switch (mode) { + | SimpleChat => + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: new_chat, + past_simple_chats: model.chats.past_simple_chats @ [new_chat], + }, + } + |> Updated.return_quiet + | CodeSuggestion => + Model.{ + ...model, + chats: { + ...model.chats, + curr_suggestion_chat: new_chat, + past_suggestion_chats: + model.chats.past_suggestion_chats @ [new_chat], + }, + } + |> Updated.return_quiet + | TaskCompletion => + Model.{ + ...model, + chats: { + ...model.chats, + curr_completion_chat: new_chat, + past_completion_chats: + model.chats.past_completion_chats @ [new_chat], + }, + } + |> Updated.return_quiet + }; + | History => + {...model, show_history: !model.show_history} |> Updated.return_quiet + | Respond(message, mode) => + check_descriptor( + ~model, + ~settings=settings.assistant, + ~editor, + ~schedule_action, + ~message, + ~mode, + ); + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: { + ...model.chats.curr_simple_chat, + messages: + mode == SimpleChat + ? model.chats.curr_simple_chat.messages @ [message] + : model.chats.curr_simple_chat.messages, + }, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? model.chats.curr_suggestion_chat.messages @ [message] + : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? model.chats.curr_completion_chat.messages @ [message] + : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + | SendSketch(tileId, mode) => + let sketch_seg = + Zipper.smart_seg( + ~dump_backpack=true, + ~erase_buffer=true, + editor.editor.state.zipper, + ); + switch ( + { + let* index = Indicated.index(editor.editor.state.zipper); + let* ci = Id.Map.find_opt(index, editor.statics.info_map); + ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); + } + ) { + | None => + print_endline("prompt generation failed"); + model |> Updated.return_quiet; + | Some(openrouter_prompt) => + let messages = + List.map( + (msg: OpenRouter.message): string => {msg.content}, + openrouter_prompt, + ); + let prompt = ListUtil.concat_strings(messages); + let message: Model.message = { + party: LS, + code: Some(sketch_seg), + content: prompt, + collapsed: String.length(prompt) >= 200, + }; + let collected_chat = + switch (mode) { + | CodeSuggestion => + collect_chat( + ~messages=model.chats.curr_suggestion_chat.messages @ [message], + ) + | TaskCompletion => + collect_chat( + ~messages=model.chats.curr_completion_chat.messages @ [message], + ) + | _ => + print_endline("Invalid mode"); + ""; + }; + print_endline(collected_chat); + let llm = model.llm; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + let index = + Option.get(Indicated.index(editor.editor.state.zipper)); + let ci = + Option.get(Id.Map.find_opt(index, editor.statics.info_map)); + schedule_action( + ErrorRespond( + content, + ci, + ChatLSP.Options.init.error_rounds_max, + tileId, + mode, + ), + ); + | None => print_endline("Assistant: response parse failed") + } + ); + Model.{ + ...model, + chats: { + ...model.chats, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? model.chats.curr_suggestion_chat.messages + @ [message, await_llm_response] + : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? model.chats.curr_completion_chat.messages + @ [message, await_llm_response] + : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + }; + | ErrorRespond(response, ci, fuel, tileId, mode) => + let message = code_message_of_str(settings, editor, response, LLM); + switch (ChatLSP.Prompt.mk_error(ci, response)) { + | None => + print_endline("ERROR ROUNDS (Non-error Response): " ++ response); + check_descriptor( + ~model, + ~settings=settings.assistant, + ~editor, + ~schedule_action, + ~message, + ~mode, + ); + schedule_action(RemoveAndSuggest(response, tileId)); + | Some(error) => + print_endline("ERROR ROUNDS (Error): " ++ error); + print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); + schedule_action(SendError(error, ci, fuel - 1, tileId, mode)); + }; + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: { + ...model.chats.curr_simple_chat, + messages: + mode == SimpleChat + ? ListUtil.leading(model.chats.curr_simple_chat.messages) + @ [message] + : model.chats.curr_simple_chat.messages, + }, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) + @ [message] + : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? ListUtil.leading(model.chats.curr_completion_chat.messages) + @ [message] + : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + | SendError(error, ci, fuel, tileId, mode) => + let error_message = + text_message_of_str( + "Your previous response caused the following error. Please fix it in your response: " + ++ error, + LS, + ); + // check that fuel is not 0 + if (fuel <= 0) { + schedule_action( + Respond( + text_message_of_str("Error round limit reached, stopping", LLM), + mode, + ), + ); + } else { + let collected_chat = + switch (mode) { + | SimpleChat => + collect_chat( + ~messages= + model.chats.curr_simple_chat.messages @ [error_message], + ) + | CodeSuggestion => + collect_chat( + ~messages= + model.chats.curr_suggestion_chat.messages @ [error_message], + ) + | TaskCompletion => + collect_chat( + ~messages= + model.chats.curr_completion_chat.messages @ [error_message], + ) + }; + switch (Oracle.ask(collected_chat)) { + | None => print_endline("Oracle: prompt generation failed") + | Some(openrouter_prompt) => + let llm = model.llm; + let key = Store.Generic.load("API"); + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action(ErrorRespond(content, ci, fuel, tileId, mode)) + | None => print_endline("Assistant: response parse failed") + } + ); + }; + }; + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: { + ...model.chats.curr_simple_chat, + messages: + mode == SimpleChat + ? ListUtil.leading(model.chats.curr_simple_chat.messages) + @ [error_message] + : model.chats.curr_simple_chat.messages, + }, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) + @ [error_message] + : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? ListUtil.leading(model.chats.curr_completion_chat.messages) + @ [error_message] + : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + | ToggleCollapse(index) => + let mode = settings.assistant.mode; + let updated_chat = + List.mapi( + (i: int, msg: Model.message) => + if (i == index) { + {...msg, collapsed: !msg.collapsed}; + } else { + msg; + }, + switch (mode) { + | SimpleChat => model.chats.curr_simple_chat.messages + | CodeSuggestion => model.chats.curr_suggestion_chat.messages + | TaskCompletion => model.chats.curr_completion_chat.messages + }, + ); + Model.{ + ...model, + chats: { + ...model.chats, + curr_simple_chat: { + ...model.chats.curr_simple_chat, + messages: + mode == SimpleChat + ? updated_chat : model.chats.curr_simple_chat.messages, + }, + curr_suggestion_chat: { + ...model.chats.curr_suggestion_chat, + messages: + mode == CodeSuggestion + ? updated_chat : model.chats.curr_suggestion_chat.messages, + }, + curr_completion_chat: { + ...model.chats.curr_completion_chat, + messages: + mode == TaskCompletion + ? updated_chat : model.chats.curr_completion_chat.messages, + }, + }, + } + |> Updated.return_quiet; + | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet + | RemoveAndSuggest(response, tileId) => + // Only side effects in the editor are performed here + add_suggestion(~response, tileId); + model |> Updated.return_quiet; + | SwitchMode(mode) => + {...model, show_history: false} |> Updated.return_quiet + | Describe(content, mode, chat_id) => + let find_by_id = (list, id, ~get_id) => { + List.find_opt(item => get_id(item) == id, list); + }; + + let updated_chats = + switch (mode) { + | SimpleChat => + // Only update the descriptor of the specific chat with matching ID + { + ...model.chats, + curr_simple_chat: + model.chats.curr_simple_chat.id == chat_id + ? {...model.chats.curr_simple_chat, descriptor: content} + : model.chats.curr_simple_chat, + past_simple_chats: + List.map( + (c: Model.chat) => + c.id == chat_id ? {...c, descriptor: content} : c, + model.chats.past_simple_chats, + ), + } + | CodeSuggestion => { + ...model.chats, + curr_suggestion_chat: + model.chats.curr_suggestion_chat.id == chat_id + ? {...model.chats.curr_suggestion_chat, descriptor: content} + : model.chats.curr_suggestion_chat, + past_suggestion_chats: + List.map( + (c: Model.chat) => + c.id == chat_id ? {...c, descriptor: content} : c, + model.chats.past_suggestion_chats, + ), + } + | TaskCompletion => { + ...model.chats, + curr_completion_chat: + model.chats.curr_completion_chat.id == chat_id + ? {...model.chats.curr_completion_chat, descriptor: content} + : model.chats.curr_completion_chat, + past_completion_chats: + List.map( + (c: Model.chat) => + c.id == chat_id ? {...c, descriptor: content} : c, + model.chats.past_completion_chats, + ), + } + }; + + {...model, chats: updated_chats} |> Updated.return_quiet; + | SwitchChat(chat_id) => + let mode = settings.assistant.mode; + let find_by_id = + (chats: Model.chats, id: Id.t, ~get_id: Model.chat => Id.t) => { + switch (mode) { + | SimpleChat => + List.find_opt(item => get_id(item) == id, chats.past_simple_chats) + | CodeSuggestion => + List.find_opt( + item => get_id(item) == id, + chats.past_suggestion_chats, + ) + | TaskCompletion => + List.find_opt( + item => get_id(item) == id, + chats.past_completion_chats, + ) + }; + }; + + // Get the chat we're switching to + let curr_chat = + Option.get(find_by_id(model.chats, chat_id, ~get_id=chat => chat.id)); + + // Store current chat back into past_chats list + let updated_past_chats = + switch (mode) { + | SimpleChat => { + ...model.chats, + past_simple_chats: + List.map( + (chat: Model.chat) => + chat.id == model.chats.curr_simple_chat.id + ? model.chats.curr_simple_chat : chat, + model.chats.past_simple_chats, + ), + } + | CodeSuggestion => { + ...model.chats, + past_suggestion_chats: + List.map( + (chat: Model.chat) => + chat.id == model.chats.curr_suggestion_chat.id + ? model.chats.curr_suggestion_chat : chat, + model.chats.past_suggestion_chats, + ), + } + | TaskCompletion => { + ...model.chats, + past_completion_chats: + List.map( + (chat: Model.chat) => + chat.id == model.chats.curr_completion_chat.id + ? model.chats.curr_completion_chat : chat, + model.chats.past_completion_chats, + ), + } + }; + + // Now update the current chat + let final_chats = + switch (mode) { + | SimpleChat => {...updated_past_chats, curr_simple_chat: curr_chat} + | CodeSuggestion => { + ...updated_past_chats, + curr_suggestion_chat: curr_chat, + } + | TaskCompletion => { + ...updated_past_chats, + curr_completion_chat: curr_chat, + } + }; + + {...model, chats: final_chats, show_history: false} + |> Updated.return_quiet; + }; + }; +}; + +module Store = + Store.F({ + [@deriving (show({with_path: false}), yojson, sexp)] + type t = Model.t; + let default = () => Model.init; + let key = Store.Assistant; + }); diff --git a/src/haz3lweb/app/helpful-assistant/AssistantModel.re b/src/haz3lweb/app/helpful-assistant/AssistantModel.re deleted file mode 100644 index 2eb0273386..0000000000 --- a/src/haz3lweb/app/helpful-assistant/AssistantModel.re +++ /dev/null @@ -1,395 +0,0 @@ -module Sexp = Sexplib.Sexp; -open Haz3lcore; -open Util; -open Util.OptUtil.Syntax; -open StringUtil; - -module CodeModel = CodeEditable.Model; - -module Model = { - [@deriving (show({with_path: false}), sexp, yojson)] - type party = - | Prompt - | Task - | LLM - | LS; - - [@deriving (show({with_path: false}), sexp, yojson)] - type message = { - party, - code: option(Segment.t), - content: string, - collapsed: bool, - }; - - [@deriving (show({with_path: false}), sexp, yojson)] - type t = { - chat: list(message) /*To-do: Add chat ids for saving past chats*/, - currSender: party, - llm: OpenRouter.chat_models, - tile: Id.t, - }; - - [@deriving (show({with_path: false}), sexp, yojson)] - let init: t = { - chat: [], - currSender: LS, - llm: Gemini_Flash_Lite_2_0, - tile: Id.invalid, - }; -}; - -module Update = { - [@deriving (show({with_path: false}), sexp, yojson)] - type t = - | SendMessage(Model.message) - | SetKey(string) - | SendSketch - | SendError(string, Info.t, int) - | ErrorRespond(string, Info.t, int) - | NewChat - | Respond(Model.message) - | ToggleCollapse(int) - | SelectLLM(OpenRouter.chat_models) - | StoreTile(Id.t) - | RemoveAndSuggest(string); - - let code_message_of_str = - (settings, editor: CodeModel.t, response: string, party: Model.party) - : Model.message => { - /* Alternate method using Detruct and Insert. We need a memory of cursor location for this however. - let z = editor.editor.state.zipper; - let z = Option.get(Destruct.go(Direction.Left, z)); - let z = Option.get(Destruct.go(Direction.Left, z)); - let z = Option.get(Insert.go(response, z)); - let segment_of_response = - Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); - { - party, - code: Some(segment_of_response), - content: response, - collapsed: String.length(response) >= 200, - }; */ - // Hack(Russ) Uses same logic Andrew uses in Oracle.re to remove "??" - let string_of_sketch = - Printer.zipper_to_string(editor.editor.state.zipper); - let sketch_with_response = - Str.global_replace(Str.regexp("\\?\\?"), response, string_of_sketch); - let zipper_of_response = Printer.zipper_of_string(sketch_with_response); - switch (zipper_of_response) { - | Some(z) => - let segment_of_response = - Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); - { - party, - code: Some(segment_of_response), - content: response, - collapsed: String.length(response) >= 200, - }; - | None => { - party, - code: None, - content: response, - collapsed: String.length(response) >= 200, - } - }; - }; - - let text_message_of_str = - (response: string, party: Model.party): Model.message => { - { - party, - code: None, - content: response, - collapsed: String.length(response) >= 200, - }; - }; - - let react = - ( - ~settings, - ~editor: CodeModel.t, - ~response: string, - ~code_suggestion: bool, - ) - : t => { - // let response = response |> sanitize_response |> quote; - code_suggestion - ? Respond(code_message_of_str(settings, editor, response, LLM)) - : Respond(text_message_of_str(response, LLM)); - }; - - let await_llm_response: Model.message = { - party: LLM, - code: None, - content: "...", - collapsed: false, - }; - - let collect_chat = (~messages: list(Model.message)): string => { - let chat = "The following is a log of the current conversation. This is solely for the purpose - to help you recall the entire conversation, in case the user asks you something that needs context - from before. You should respond as normal, using the entire chat as context, and understand that the - most recent \"User Input\" is what the user is currently sending/asking, and is what your main focus should be. - For the most part, you should treat this solely as a prompt, and not explicitly acknowledge it in your - reponse. Here is the conversation for context: "; - List.fold_left( - (chat: string, message: Model.message) => - if (message.party == LLM) { - chat ++ "Your Reponse: " ++ message.content ++ " "; - } else if (message.party == LS) { - chat ++ "User Input: " ++ message.content ++ " "; - } else { - chat ++ message.content; - }, - chat, - messages, - ); - }; - - let check_req = (_: string, schedule_action: t => unit, z: Zipper.t): unit => { - let caret = z.caret; - let siblings = z.relatives.siblings; - switch (caret, Zipper.neighbor_monotiles(siblings)) { - | (Outer, (_, Some(_))) => - switch (Zipper.right_neighbor_monotile(siblings)) { - | Some(c) => - c == "??" - ? { - let id = Option.get(Indicated.index(z)); - schedule_action(StoreTile(id)); - schedule_action(SendSketch); - } - : () - | _ => () - } - | (Outer, (_, None)) => - switch (Zipper.left_neighbor_monotile(siblings)) { - | Some(c) => - c == "??" - ? { - let id = Option.get(Indicated.index(z)); - schedule_action(StoreTile(id)); - schedule_action(SendSketch); - } - : () - | _ => () - } - | _ => () - }; - }; - - let set_buffer = (~response: string, z: Zipper.t): option(Zipper.t) => { - let zipper_of_response = Option.get(Printer.zipper_of_string(response)); - let seg_of_response = - Zipper.smart_seg( - ~dump_backpack=true, - ~erase_buffer=true, - zipper_of_response, - ); - let z = Zipper.set_buffer(z, ~content=seg_of_response, ~mode=Unparsed); - Some(z); - }; - - let update = - ( - ~settings, - ~action, - ~editor: CodeModel.t, - ~model: Model.t, - ~schedule_action, - ~add_suggestion, - ) - : Updated.t(Model.t) => { - switch (action) { - | SendMessage(message) => - switch (message.party) { - | LS => - let collected_chat = collect_chat(~messages=model.chat @ [message]); - print_endline(collected_chat); - switch (Oracle.ask(collected_chat)) { - | None => print_endline("Oracle: prompt generation failed") - | Some(prompt) => - let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - schedule_action( - react( - ~settings, - ~editor, - ~response=content, - ~code_suggestion=false, - ), - ) - | None => print_endline("Assistant: response parse failed") - } - ); - }; - Model.{ - ...model, - chat: model.chat @ [message, await_llm_response], - currSender: LLM, - } - |> Updated.return_quiet; - | _ => - Model.{...model, chat: model.chat, currSender: LLM} - |> Updated.return_quiet - } - | SetKey(api_key) => - Store.Generic.save("API", api_key); - model |> Updated.return_quiet; - | NewChat => - Model.{...model, chat: [], currSender: LS} |> Updated.return_quiet - | Respond(message) => - Model.{ - ...model, - chat: ListUtil.leading(model.chat) @ [message], - currSender: LS, - } - |> Updated.return_quiet - | SendSketch => - let sketch_seg = - Zipper.smart_seg( - ~dump_backpack=true, - ~erase_buffer=true, - editor.editor.state.zipper, - ); - switch ( - { - let* index = Indicated.index(editor.editor.state.zipper); - let* ci = Id.Map.find_opt(index, editor.statics.info_map); - ChatLSP.Prompt.mk_init(ChatLSP.Options.init, ci, sketch_seg); - } - ) { - | None => - print_endline("prompt generation failed"); - Model.{...model, chat: model.chat, currSender: LLM} - |> Updated.return_quiet; - | Some(openrouter_prompt) => - let messages = - List.map( - (msg: OpenRouter.message): string => {msg.content}, - openrouter_prompt, - ); - let prompt = ListUtil.concat_strings(messages); - let message: Model.message = { - party: LS, - code: Some(sketch_seg), - content: prompt, - collapsed: String.length(prompt) >= 200, - }; - let collected_chat = collect_chat(~messages=model.chat @ [message]); - print_endline(collected_chat); - let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - let index = - Option.get(Indicated.index(editor.editor.state.zipper)); - let ci = - Option.get(Id.Map.find_opt(index, editor.statics.info_map)); - schedule_action( - ErrorRespond( - content, - ci, - ChatLSP.Options.init.error_rounds_max, - ), - ); - | None => print_endline("Assistant: response parse failed") - } - ); - Model.{ - ...model, - chat: model.chat @ [message, await_llm_response], - currSender: LLM, - } - |> Updated.return_quiet; - }; - | ErrorRespond(response, ci, fuel) => - let message = code_message_of_str(settings, editor, response, LLM); - switch (ChatLSP.Prompt.mk_error(ci, response)) { - | None => - print_endline("ERROR ROUNDS (Non-error Response): " ++ response); - schedule_action(RemoveAndSuggest(response)); - | Some(error) => - print_endline("ERROR ROUNDS (Error): " ++ error); - print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); - schedule_action(SendError(error, ci, fuel - 1)); - }; - Model.{ - ...model, - chat: ListUtil.leading(model.chat) @ [message], - currSender: LS, - } - |> Updated.return_quiet; - | SendError(error, ci, fuel) => - let error_message = - text_message_of_str( - "Your previous response caused the following error. Please fix it in your response: " - ++ error, - LS, - ); - // check that fuel is not 0 - if (fuel <= 0) { - schedule_action( - Respond( - text_message_of_str("Error round limit reached, stopping", LLM), - ), - ); - } else { - let collected_chat = - collect_chat(~messages=model.chat @ [error_message]); - switch (Oracle.ask(collected_chat)) { - | None => print_endline("Oracle: prompt generation failed") - | Some(openrouter_prompt) => - let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - schedule_action(ErrorRespond(content, ci, fuel)) - | None => print_endline("Assistant: response parse failed") - } - ); - }; - }; - Model.{ - ...model, - chat: model.chat @ [error_message, await_llm_response], - currSender: LLM, - } - |> Updated.return_quiet; - | ToggleCollapse(index) => - let updated_chat = - List.mapi( - (i: int, msg: Model.message) => - if (i == index) { - {...msg, collapsed: !msg.collapsed}; - } else { - msg; - }, - model.chat, - ); - Model.{...model, chat: updated_chat} |> Updated.return_quiet; - | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet - | StoreTile(id) => {...model, tile: id} |> Updated.return_quiet - | RemoveAndSuggest(response) => - add_suggestion(~response, model.tile); - {...model, tile: Id.invalid} |> Updated.return_quiet; - }; - }; -}; - -module Store = - Store.F({ - [@deriving (show({with_path: false}), yojson, sexp)] - type t = Model.t; - let default = () => Model.init; - let key = Store.Assistant; - }); diff --git a/src/haz3lweb/app/helpful-assistant/AssistantSettings.re b/src/haz3lweb/app/helpful-assistant/AssistantSettings.re index ca744efaa4..4123ca601d 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantSettings.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantSettings.re @@ -3,24 +3,22 @@ open Haz3lcore; open Util; [@deriving (show({with_path: false}), sexp, yojson)] -type manual_llm = - | Agent - | Human; - -[@deriving (show({with_path: false}), sexp, yojson)] -type manual_lsp = - | LanguageServer - | Human; +type mode = + | CodeSuggestion + | TaskCompletion + | SimpleChat; [@deriving (show({with_path: false}), sexp, yojson)] type t = { llm: bool, lsp: bool, ongoing_chat: bool, + mode, }; [@deriving (show({with_path: false}), sexp, yojson)] type action = | ToggleLLM | ToggleLSP - | UpdateChatStatus; + | UpdateChatStatus + | SwitchMode(mode); diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index d657f43c4d..87259a4e5f 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -59,7 +59,7 @@ let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { let begin_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), - inject(AssistantModel.Update.NewChat), + inject(Assistant.Update.NewChat), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( @@ -69,7 +69,7 @@ let begin_chat_button = (~globals: Globals.t, ~inject): Node.t => { }; let resume_chat_button = (~globals: Globals.t): Node.t => { - let tooltip = "Previous Chat"; + let tooltip = "Confirm and Chat"; let resume_chat = _ => Virtual_dom.Vdom.Effect.Many([ globals.inject_global(Set(Assistant(UpdateChatStatus))), @@ -81,33 +81,43 @@ let resume_chat_button = (~globals: Globals.t): Node.t => { ); }; -let req_button = (~inject): Node.t => { - let tooltip = "??"; - let send_sketch = _ => +let end_chat_button = (~globals: Globals.t): Node.t => { + let tooltip = "Settings"; + let end_chat = _ => Virtual_dom.Vdom.Effect.Many([ - inject(AssistantModel.Update.SendSketch), + globals.inject_global(Set(Assistant(UpdateChatStatus))), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( - ~attrs=[clss(["chat-button"]), Attr.on_click(send_sketch)], - [Widgets.button_named(~tooltip, None, send_sketch)], + ~attrs=[clss(["chat-button"])], + [Widgets.button_named(~tooltip, None, end_chat)], ); }; -let end_chat_button = (~globals: Globals.t): Node.t => { - let tooltip = "End Chat"; - let end_chat = _ => +let new_chat_button = (~globals: Globals.t, ~inject): Node.t => { + let tooltip = "New Chat"; + let new_chat = _ => Virtual_dom.Vdom.Effect.Many([ - globals.inject_global(Set(Assistant(UpdateChatStatus))), + inject(Assistant.Update.NewChat), Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( - ~attrs=[clss(["chat-button"]), Attr.on_click(end_chat)], - [Widgets.button_named(~tooltip, None, end_chat)], + ~attrs=[clss(["add-button"])], + [Widgets.button(~tooltip, Icons.add, new_chat)], ); }; -let select_llm = (~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { +let history_button = (~globals: Globals.t, ~inject): Node.t => { + let tooltip = "Past Chats"; + let history = _ => + Virtual_dom.Vdom.Effect.Many([inject(Assistant.Update.History)]); + div( + ~attrs=[clss(["history-button"])], + [Widgets.button(~tooltip, Icons.history, history)], + ); +}; + +let select_llm = (~inject, ~assistantModel: Assistant.Model.t): Node.t => { let handle_change = (event, _) => { let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); let selected_llm = @@ -119,7 +129,7 @@ let select_llm = (~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { | _ => OpenRouter.Gemini_Flash_Lite_2_0 }; Virtual_dom.Vdom.Effect.Many([ - inject(AssistantModel.Update.SelectLLM(selected_llm)), + inject(Assistant.Update.SelectLLM(selected_llm)), Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; @@ -187,18 +197,24 @@ let settings_box = (~globals: Globals.t, ~inject): Node.t => { [ // llm_toggle(~globals), // lsp_toggle(~globals), - begin_chat_button(~globals, ~inject), + // begin_chat_button(~globals, ~inject), resume_chat_button(~globals), ], ); }; let api_input = - (~signal, ~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { + ( + ~signal, + ~inject, + ~assistantModel: Assistant.Model.t, + ~settings: AssistantSettings.t, + ) + : Node.t => { let handle_submission = (api_key: string) => { JsUtil.log("Your API key for this session has been set: " ++ api_key); Virtual_dom.Vdom.Effect.Many([ - inject(AssistantModel.Update.SetKey(api_key)), + inject(Assistant.Update.SetKey(api_key)), Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; @@ -221,7 +237,16 @@ let api_input = }; let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key, ListUtil.last_opt(assistantModel.chat)) { + switch ( + key, + ListUtil.last_opt( + switch (settings.mode) { + | SimpleChat => assistantModel.chats.curr_simple_chat.messages + | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages + | TaskCompletion => assistantModel.chats.curr_completion_chat.messages + }, + ), + ) { | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore | (Some("Enter"), _) => submit_key() | _ => Virtual_dom.Vdom.Effect.Ignore @@ -266,17 +291,23 @@ let api_input = }; let message_input = - (~signal, ~inject, ~assistantModel: AssistantModel.Model.t): Node.t => { + ( + ~signal, + ~inject, + ~assistantModel: Assistant.Model.t, + ~settings: AssistantSettings.t, + ) + : Node.t => { let handle_send = (message: string) => { - let message: AssistantModel.Model.message = { - party: assistantModel.currSender, + let message: Assistant.Model.message = { + party: LS, code: None, content: message, collapsed: String.length(message) >= 200, }; JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ - inject(AssistantModel.Update.SendMessage(message)), + inject(Assistant.Update.SendMessage(message)), Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; @@ -300,7 +331,16 @@ let message_input = }; let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key, ListUtil.last_opt(assistantModel.chat)) { + switch ( + key, + ListUtil.last_opt( + switch (settings.mode) { + | SimpleChat => assistantModel.chats.curr_simple_chat.messages + | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages + | TaskCompletion => assistantModel.chats.curr_completion_chat.messages + }, + ), + ) { | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore | (Some("Enter"), _) => send_message() | _ => Virtual_dom.Vdom.Effect.Ignore @@ -312,7 +352,13 @@ let message_input = input( ~attrs=[ Attr.id("message-input"), - Attr.placeholder("Type a message..."), + Attr.placeholder( + switch (settings.mode) { + | SimpleChat => "Ask anything..." + | CodeSuggestion => "Followup with a question..." + | TaskCompletion => "Type a task completion..." + }, + ), Attr.type_("text"), Attr.property("autocomplete", Js.Unsafe.inject("off")), Attr.on_focus(_ => @@ -323,7 +369,17 @@ let message_input = ], (), ), - switch (ListUtil.last_opt(assistantModel.chat)) { + switch ( + ListUtil.last_opt( + switch (settings.mode) { + | SimpleChat => assistantModel.chats.curr_simple_chat.messages + | CodeSuggestion => + assistantModel.chats.curr_suggestion_chat.messages + | TaskCompletion => + assistantModel.chats.curr_completion_chat.messages + }, + ) + ) { | Some({party: LLM, code: None, content: "...", collapsed: false}) => div( ~attrs=[ @@ -358,12 +414,17 @@ let loading_dots = () => { }; let message_display = - (~inject, ~globals: Globals.t, ~assistantModel: AssistantModel.Model.t) + ( + ~inject, + ~globals: Globals.t, + ~assistantModel: Assistant.Model.t, + ~settings: AssistantSettings.t, + ) : Node.t => { let toggle_collapse = index => { // Create an action to toggle the collapsed state of a specific message Virtual_dom.Vdom.Effect.Many([ - inject(AssistantModel.Update.ToggleCollapse(index)), + inject(Assistant.Update.ToggleCollapse(index)), Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; @@ -371,7 +432,7 @@ let message_display = let message_nodes = List.flatten( List.mapi( - (index: int, message: AssistantModel.Model.message) => { + (index: int, message: Assistant.Model.message) => { switch (message.code) { | Some(sketch) => message.content == "..." && message.party == LLM @@ -386,6 +447,10 @@ let message_display = Attr.on_click(_ => toggle_collapse(index)), ], [ + div( + ~attrs=[clss(["message-identifier"])], + [text(message.party == LLM ? "LLM" : "LS")], + ), div( ~attrs=[ clss([ @@ -429,18 +494,25 @@ let message_display = ~selected=None, ~caption=None, ~locked=true, - { - sketch - |> Zipper.unzip - |> Editor.Model.mk - |> CellEditor.Model.mk - |> CellEditor.Update.calculate( - ~settings=globals.settings.core, - ~is_edited=true, - ~stitch=x => x, - ~queue_worker=None, - ); - }, + message.party == LLM + ? { + sketch + |> Zipper.unzip + |> Editor.Model.mk + |> CellEditor.Model.mk; + } + : { + sketch + |> Zipper.unzip + |> Editor.Model.mk + |> CellEditor.Model.mk + |> CellEditor.Update.calculate( + ~settings=globals.settings.core, + ~is_edited=true, + ~stitch=x => x, + ~queue_worker=None, + ); + }, ), ], ), @@ -458,6 +530,10 @@ let message_display = Attr.on_click(_ => toggle_collapse(index)), ], [ + div( + ~attrs=[clss(["message-identifier"])], + [text(message.party == LLM ? "Assistant" : "User")], + ), div( ~attrs=[ clss([ @@ -488,18 +564,92 @@ let message_display = ] } }, - assistantModel.chat, + switch (settings.mode) { + | SimpleChat => assistantModel.chats.curr_simple_chat.messages + | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages + | TaskCompletion => assistantModel.chats.curr_completion_chat.messages + }, ), ); div(~attrs=[clss(["message-display-container"])], message_nodes); }; +let mode_buttons = (~globals: Globals.t): Node.t => { + let mode_button = (mode: AssistantSettings.mode, label: string) => { + let switch_mode = _ => + Virtual_dom.Vdom.Effect.Many([ + globals.inject_global(Set(Assistant(SwitchMode(mode)))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[ + clss([ + "mode-button", + globals.settings.assistant.mode == mode ? "active" : "", + ]), + Attr.on_click(switch_mode), + ], + [text(label)], + ); + }; + + div( + ~attrs=[clss(["mode-buttons"])], + [ + mode_button(SimpleChat, "Chat"), + mode_button(CodeSuggestion, "Suggest"), + mode_button(TaskCompletion, "Compose"), + ], + ); +}; + +let history_menu = + ( + ~assistantModel: Assistant.Model.t, + ~settings: AssistantSettings.t, + ~inject, + ) + : Node.t => { + let past_chats = + switch (settings.mode) { + | SimpleChat => assistantModel.chats.past_simple_chats + | CodeSuggestion => assistantModel.chats.past_suggestion_chats + | TaskCompletion => assistantModel.chats.past_completion_chats + }; + + div( + ~attrs=[clss(["history-menu"])], + [ + div(~attrs=[clss(["history-menu-header"])], [text("Chat History")]), + div( + ~attrs=[clss(["history-menu-list"])], + List.map( + (chat: Assistant.Model.chat) => + div( + ~attrs=[ + clss(["history-menu-item"]), + Attr.on_click(_ => + Virtual_dom.Vdom.Effect.Many([ + inject(Assistant.Update.SwitchChat(chat.id)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + ), + ], + [text(chat.descriptor == "" ? "New chat" : chat.descriptor)], + ), + past_chats, + ), + ), + ], + ); +}; + let view = ( ~globals: Globals.t, ~signal, ~inject, - ~assistantModel: AssistantModel.Model.t, + ~assistantModel: Assistant.Model.t, ) => { div( ~attrs=[Attr.id("side-bar")], @@ -511,25 +661,61 @@ let view = ~attrs=[clss(["header"])], [ div( - ~attrs=[clss(["title"])], - [text("Agentic Assistant Chat")], + ~attrs=[clss(["header-content"])], + [ + globals.settings.assistant.ongoing_chat + ? mode_buttons(~globals) : text("Assistant Settings"), + div( + ~attrs=[clss(["header-actions"])], + [ + globals.settings.assistant.ongoing_chat + ? history_button(~globals, ~inject) : None, + globals.settings.assistant.ongoing_chat + ? new_chat_button(~globals, ~inject) : None, + globals.settings.assistant.ongoing_chat + ? end_chat_button(~globals) : None, + ], + ), + ], ), - globals.settings.assistant.ongoing_chat - ? req_button(~inject) : None, - globals.settings.assistant.ongoing_chat - ? end_chat_button(~globals) : None, ], ), globals.settings.assistant.ongoing_chat - ? message_display(~inject, ~globals, ~assistantModel) : None, + ? message_display( + ~inject, + ~globals, + ~assistantModel, + ~settings=globals.settings.assistant, + ) + : None, globals.settings.assistant.ongoing_chat - ? message_input(~signal, ~inject, ~assistantModel) : None, + ? message_input( + ~signal, + ~inject, + ~assistantModel, + ~settings=globals.settings.assistant, + ) + : None, globals.settings.assistant.ongoing_chat - ? None : api_input(~signal, ~inject, ~assistantModel), + ? None + : api_input( + ~signal, + ~inject, + ~assistantModel, + ~settings=globals.settings.assistant, + ), globals.settings.assistant.ongoing_chat ? None : select_llm(~inject, ~assistantModel), globals.settings.assistant.ongoing_chat ? None : settings_box(~globals, ~inject), + globals.settings.assistant.ongoing_chat + && assistantModel.show_history + ? history_menu( + ~assistantModel, + ~settings=globals.settings.assistant, + ~inject, + ) + : None, ], ), ], diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index e7f44ad9c4..74ea8cc990 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -16,7 +16,7 @@ module Model = { globals: Globals.Model.t, editors: Editors.Model.t, explain_this: ExplainThisModel.t, - assistant: AssistantModel.Model.t, + assistant: Assistant.Model.t, selection, }; @@ -32,7 +32,7 @@ module Store = { ~instructor_mode=globals.settings.instructor_mode, ); let explain_this = ExplainThisModel.Store.load(); - let assistant = AssistantModel.Store.load(); + let assistant = Assistant.Store.load(); { editors, globals, @@ -49,7 +49,7 @@ module Store = { ); Globals.Model.save(m.globals); ExplainThisModel.Store.save(m.explain_this); - AssistantModel.Store.save(m.assistant); + Assistant.Store.save(m.assistant); }; }; @@ -66,7 +66,7 @@ module Update = { | Globals(Globals.Update.t) | Editors(Editors.Update.t) | ExplainThis(ExplainThisUpdate.update) - | Assistant(AssistantModel.Update.t) + | Assistant(Assistant.Update.t) | MakeActive(selection) | Benchmark(benchmark_action) | Start @@ -83,7 +83,7 @@ module Update = { /* A function passed down to trigger an update within assistant which checks for the insertion of '??' */ let send_insertion_info = (~char, ~editor) => { - AssistantModel.Update.check_req( + Assistant.Update.check_req( char, a => schedule_action(Assistant(a)), editor, @@ -231,7 +231,7 @@ module Update = { /* A function passed down to trigger an update within assistant which checks for the insertion of '??' */ let send_insertion_info = (~char, ~editor) => { - AssistantModel.Update.check_req( + Assistant.Update.check_req( char, a => schedule_action(Assistant(a)), editor, @@ -288,7 +288,7 @@ module Update = { ); }; let* assistant = - AssistantModel.Update.update( + Assistant.Update.update( ~settings, ~action, ~editor=ed.editor, diff --git a/src/haz3lweb/view/ScratchMode.re b/src/haz3lweb/view/ScratchMode.re index 395ed6bc2b..3e76c7742f 100644 --- a/src/haz3lweb/view/ScratchMode.re +++ b/src/haz3lweb/view/ScratchMode.re @@ -101,11 +101,7 @@ module Update = { switch (action) { | Perform(a) => switch (a) { - | Insert(char) => - send_insertion_info( - ~char, - ~editor=new_ed.editor.editor.state.zipper, - ) + | Insert(char) => send_insertion_info(~char, ~editor=new_ed.editor) | _ => () } | _ => () diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 0326b79e5d..8fcccd59a0 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -25,9 +25,15 @@ display: flex; text-transform: none; font-weight: bold; + margin-bottom: 1em; + width: 100%; +} + +#assistant .header-content { + display: flex; + width: 100%; justify-content: space-between; align-items: center; - margin-bottom: 1em; } #assistant .settings-box { @@ -53,6 +59,7 @@ height: 1.25em; cursor: pointer; transition: all 0.3s ease; + margin: 4px; } #assistant .chat-button:hover { @@ -134,10 +141,34 @@ /* Individual message container */ #assistant .message-container { display: flex; - align-items: center; + flex-direction: column; margin-bottom: 10px; } +#assistant .message-identifier { + font-size: 0.8em; + color: var(--STONE); + opacity: 0.7; + margin-bottom: 4px; + padding: 0 12px; +} + +#assistant .llm .message-identifier { + align-self: flex-start; +} + +#assistant .ls .message-identifier { + align-self: flex-end; +} + +#assistant .llm { + align-items: flex-start; +} + +#assistant .ls { + align-items: flex-end; +} + #assistant .collapse-indicator { width: 100%; text-align: center; @@ -155,14 +186,6 @@ content: "▲ Show less"; } -#assistant .llm { - justify-content: none; -} - -#assistant .ls { - justify-content: flex-end; -} - #assistant .llm-message { padding: 8px 12px; background-color: var(--T1); @@ -332,4 +355,143 @@ .llm-dropdown:focus { outline: none; border-color: #555; +} + +/* Mode buttons styling */ +#assistant .mode-buttons { + display: flex; + align-items: center; + gap: 4px; +} + +#assistant .mode-button { + padding: 0.35em 0.75em; + font-size: 0.75em; + color: var(--STONE); + cursor: pointer; + border-radius: 3px; + transition: all 0.2s ease; + text-align: center; +} + +#assistant .header-actions { + display: flex; + align-items: center; + gap: 4px; +} + +#assistant .mode-button:hover { + opacity: 0.7; +} + +#assistant .mode-button.active { + text-decoration: underline; + text-underline-offset: 8px; + color: var(--STONE); +} + +#assistant .add-button, +#assistant .history-button { + color: var(--STONE); + padding: .45em .5em; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.3s ease; + line-height: 0; +} + +#assistant .add-button:hover, +#assistant .history-button:hover { + background-color: var(--T1); + box-shadow: 0px 2px 4px var(--SHADOW); + opacity: 0.7; +} + +#assistant .add-button:active, +#assistant .history-button:active { + background-color: var(--T3); + opacity: 0.5; +} + +/* History Menu Styling */ +#assistant .history-menu { + position: absolute; + top: 3.5em; + right: 32.2em; + width: 250px; + background-color: var(--UI-Background); + border: 1px solid var(--SAND); + border-radius: 4px; + box-shadow: 0 4px 12px var(--SHADOW); + z-index: 1000; + overflow: hidden; + animation: slideIn 0.2s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +#assistant .history-menu-header { + padding: 12px 16px; + background-color: var(--T1); + color: var(--STONE); + font-weight: bold; + border-bottom: 1px solid var(--SAND); + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +#assistant .history-menu-list { + max-height: 300px; + overflow-y: auto; + padding: 4px 0; +} + +#assistant .history-menu-list::-webkit-scrollbar { + width: 8px; +} + +#assistant .history-menu-list::-webkit-scrollbar-track { + background: var(--UI-Background); +} + +#assistant .history-menu-list::-webkit-scrollbar-thumb { + background: var(--SAND); + border-radius: 4px; +} + +#assistant .history-menu-item { + padding: 10px 16px; + color: var(--STONE); + cursor: pointer; + transition: all 0.2s ease; + border-bottom: 1px solid var(--SAND); + font-size: 0.9em; + display: flex; + align-items: center; +} + +#assistant .history-menu-item:last-child { + border-bottom: none; +} + +#assistant .history-menu-item:hover { + background-color: var(--T1); + padding-left: 20px; +} + +#assistant .history-menu-item:active { + background-color: var(--T3); } \ No newline at end of file From 5397bb5baf055d3945f66e39ba838f053dc77ccb Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 1 Mar 2025 13:42:09 -0500 Subject: [PATCH 41/50] adds timestamps and deletions of chats; fixes a few bugs with chat logs and storage of them --- src/haz3lcore/dune | 2 +- src/haz3lweb/app/common/Icons.re | 12 + .../app/helpful-assistant/Assistant.re | 215 ++++++++++++++---- .../app/helpful-assistant/AssistantUtil.re | 30 +++ .../app/helpful-assistant/AssistantView.re | 62 ++++- src/haz3lweb/www/style/assistant.css | 92 ++++++-- 6 files changed, 342 insertions(+), 71 deletions(-) create mode 100644 src/haz3lweb/app/helpful-assistant/AssistantUtil.re diff --git a/src/haz3lcore/dune b/src/haz3lcore/dune index a0d9770816..edbbffa413 100644 --- a/src/haz3lcore/dune +++ b/src/haz3lcore/dune @@ -2,7 +2,7 @@ (library (name haz3lcore) - (libraries util sexplib unionFind uuidm virtual_dom yojson core) + (libraries util unix sexplib unionFind uuidm virtual_dom yojson core) (js_of_ocaml) (instrumentation (backend bisect_ppx)) diff --git a/src/haz3lweb/app/common/Icons.re b/src/haz3lweb/app/common/Icons.re index d68f12c274..d9fc7341e0 100644 --- a/src/haz3lweb/app/common/Icons.re +++ b/src/haz3lweb/app/common/Icons.re @@ -301,3 +301,15 @@ let history = "M5.07868 5.06891C8.87402 1.27893 15.0437 1.31923 18.8622 5.13778C22.6824 8.95797 22.7211 15.1313 18.9262 18.9262C15.1312 22.7211 8.95793 22.6824 5.13774 18.8622C2.87389 16.5984 1.93904 13.5099 2.34047 10.5812C2.39672 10.1708 2.775 9.88377 3.18537 9.94002C3.59575 9.99627 3.88282 10.3745 3.82658 10.7849C3.4866 13.2652 4.27782 15.881 6.1984 17.8016C9.44288 21.0461 14.6664 21.0646 17.8655 17.8655C21.0646 14.6664 21.046 9.44292 17.8015 6.19844C14.5587 2.95561 9.33889 2.93539 6.13935 6.12957L6.88705 6.13333C7.30126 6.13541 7.63535 6.47288 7.63327 6.88709C7.63119 7.3013 7.29372 7.63539 6.87951 7.63331L4.33396 7.62052C3.92269 7.61845 3.58981 7.28556 3.58774 6.8743L3.57495 4.32874C3.57286 3.91454 3.90696 3.57707 4.32117 3.57498C4.73538 3.5729 5.07285 3.907 5.07493 4.32121L5.07868 5.06891ZM11.9999 7.24992C12.4141 7.24992 12.7499 7.58571 12.7499 7.99992V11.6893L15.0302 13.9696C15.3231 14.2625 15.3231 14.7374 15.0302 15.0302C14.7373 15.3231 14.2624 15.3231 13.9696 15.0302L11.2499 12.3106V7.99992C11.2499 7.58571 11.5857 7.24992 11.9999 7.24992Z", ], ); + +let delete = + simple_icon( + ~view="0 0 24 24", + [ + "M12 2.75C11.0215 2.75 10.1871 3.37503 9.87787 4.24993C9.73983 4.64047 9.31134 4.84517 8.9208 4.70713C8.53026 4.56909 8.32557 4.1406 8.46361 3.75007C8.97804 2.29459 10.3661 1.25 12 1.25C13.634 1.25 15.022 2.29459 15.5365 3.75007C15.6745 4.1406 15.4698 4.56909 15.0793 4.70713C14.6887 4.84517 14.2602 4.64047 14.1222 4.24993C13.813 3.37503 12.9785 2.75 12 2.75Z", + "M2.75 6C2.75 5.58579 3.08579 5.25 3.5 5.25H20.5001C20.9143 5.25 21.2501 5.58579 21.2501 6C21.2501 6.41421 20.9143 6.75 20.5001 6.75H3.5C3.08579 6.75 2.75 6.41421 2.75 6Z", + "M5.91508 8.45011C5.88753 8.03681 5.53015 7.72411 5.11686 7.75166C4.70356 7.77921 4.39085 8.13659 4.41841 8.54989L4.88186 15.5016C4.96735 16.7844 5.03641 17.8205 5.19838 18.6336C5.36678 19.4789 5.6532 20.185 6.2448 20.7384C6.83639 21.2919 7.55994 21.5307 8.41459 21.6425C9.23663 21.75 10.2751 21.75 11.5607 21.75H12.4395C13.7251 21.75 14.7635 21.75 15.5856 21.6425C16.4402 21.5307 17.1638 21.2919 17.7554 20.7384C18.347 20.185 18.6334 19.4789 18.8018 18.6336C18.9637 17.8205 19.0328 16.7844 19.1183 15.5016L19.5818 8.54989C19.6093 8.13659 19.2966 7.77921 18.8833 7.75166C18.47 7.72411 18.1126 8.03681 18.0851 8.45011L17.6251 15.3492C17.5353 16.6971 17.4712 17.6349 17.3307 18.3405C17.1943 19.025 17.004 19.3873 16.7306 19.6431C16.4572 19.8988 16.083 20.0647 15.391 20.1552C14.6776 20.2485 13.7376 20.25 12.3868 20.25H11.6134C10.2626 20.25 9.32255 20.2485 8.60915 20.1552C7.91715 20.0647 7.54299 19.8988 7.26957 19.6431C6.99616 19.3873 6.80583 19.025 6.66948 18.3405C6.52891 17.6349 6.46488 16.6971 6.37503 15.3492L5.91508 8.45011Z", + "M9.42546 10.2537C9.83762 10.2125 10.2051 10.5132 10.2464 10.9254L10.7464 15.9254C10.7876 16.3375 10.4869 16.7051 10.0747 16.7463C9.66256 16.7875 9.29502 16.4868 9.25381 16.0746L8.75381 11.0746C8.71259 10.6625 9.0133 10.2949 9.42546 10.2537Z", + "M15.2464 11.0746C15.2876 10.6625 14.9869 10.2949 14.5747 10.2537C14.1626 10.2125 13.795 10.5132 13.7538 10.9254L13.2538 15.9254C13.2126 16.3375 13.5133 16.7051 13.9255 16.7463C14.3376 16.7875 14.7051 16.4868 14.7464 16.0746L15.2464 11.0746Z", + ], + ); diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index d44afc4987..71521e7f6b 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -27,6 +27,7 @@ module Model = { messages: list(message), id: Id.t, descriptor: string, + timestamp: float, }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -46,9 +47,24 @@ module Model = { show_history: bool, }; - let init_simple_chat = {messages: [], id: Id.mk(), descriptor: ""}; - let init_suggestion_chat = {messages: [], id: Id.mk(), descriptor: ""}; - let init_completion_chat = {messages: [], id: Id.mk(), descriptor: ""}; + let init_simple_chat = { + messages: [], + id: Id.mk(), + descriptor: "", + timestamp: Unix.time(), + }; + let init_suggestion_chat = { + messages: [], + id: Id.mk(), + descriptor: "", + timestamp: Unix.time(), + }; + let init_completion_chat = { + messages: [], + id: Id.mk(), + descriptor: "", + timestamp: Unix.time(), + }; [@deriving (show({with_path: false}), sexp, yojson)] let init: t = { @@ -74,6 +90,7 @@ module Update = { | SendError(string, Info.t, int, Id.t, AssistantSettings.mode) | ErrorRespond(string, Info.t, int, Id.t, AssistantSettings.mode) | NewChat + | DeleteChat(Id.t) | History | Respond(Model.message, AssistantSettings.mode) | ToggleCollapse(int) @@ -235,37 +252,44 @@ module Update = { ~mode: AssistantSettings.mode, ) : unit => { + // Only create a summary up to the first 3 exchanges switch (mode) { | SimpleChat => - form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_simple_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_simple_chat.id, - ) + List.length(model.chats.past_simple_chats) <= 6 + ? form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_simple_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_simple_chat.id, + ) + : () | CodeSuggestion => - form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_suggestion_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_suggestion_chat.id, - ) + List.length(model.chats.past_suggestion_chats) <= 6 + ? form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_suggestion_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_suggestion_chat.id, + ) + : () | TaskCompletion => - form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_completion_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_completion_chat.id, - ) + List.length(model.chats.past_completion_chats) <= 6 + ? form_descriptor( + ~model, + ~settings, + ~editor, + ~schedule_action, + ~chat=model.chats.curr_completion_chat.messages @ [message], + ~mode, + ~chat_id=model.chats.curr_completion_chat.id, + ) + : () }; }; @@ -399,21 +423,24 @@ module Update = { ...model.chats.curr_simple_chat, messages: mode == SimpleChat - ? model.chats.curr_simple_chat.messages @ [message] + ? model.chats.curr_simple_chat.messages + @ [message, await_llm_response] : model.chats.curr_simple_chat.messages, }, curr_suggestion_chat: { ...model.chats.curr_suggestion_chat, messages: mode == CodeSuggestion - ? model.chats.curr_suggestion_chat.messages @ [message] + ? model.chats.curr_suggestion_chat.messages + @ [message, await_llm_response] : model.chats.curr_suggestion_chat.messages, }, curr_completion_chat: { ...model.chats.curr_completion_chat, messages: mode == TaskCompletion - ? model.chats.curr_completion_chat.messages @ [message] + ? model.chats.curr_completion_chat.messages + @ [message, await_llm_response] : model.chats.curr_completion_chat.messages, }, }, @@ -424,7 +451,12 @@ module Update = { model |> Updated.return_quiet; | NewChat => let mode = settings.assistant.mode; - let new_chat: Model.chat = {messages: [], id: Id.mk(), descriptor: ""}; + let new_chat: Model.chat = { + messages: [], + id: Id.mk(), + descriptor: "", + timestamp: Unix.time(), + }; switch (mode) { | SimpleChat => Model.{ @@ -459,6 +491,91 @@ module Update = { } |> Updated.return_quiet }; + | DeleteChat(chat_to_be_gone_id) => + let mode = settings.assistant.mode; + // Filter out the chat we're deleting + let updated_past_chats = + switch (mode) { + | SimpleChat => { + ...model.chats, + past_simple_chats: + switch (ListUtil.last_opt(model.chats.past_simple_chats)) { + | Some(_) => + List.filter_map( + (chat: Model.chat) => + chat.id == chat_to_be_gone_id ? None : Some(chat), + model.chats.past_simple_chats, + ) + | None => model.chats.past_simple_chats + }, + } + | CodeSuggestion => { + ...model.chats, + past_suggestion_chats: + switch (ListUtil.last_opt(model.chats.past_suggestion_chats)) { + | Some(_) => + List.filter_map( + (chat: Model.chat) => + chat.id == chat_to_be_gone_id ? None : Some(chat), + model.chats.past_suggestion_chats, + ) + | None => model.chats.past_suggestion_chats + }, + } + | TaskCompletion => { + ...model.chats, + past_completion_chats: + switch (ListUtil.last_opt(model.chats.past_completion_chats)) { + | Some(_) => + List.filter_map( + (chat: Model.chat) => + chat.id == chat_to_be_gone_id ? None : Some(chat), + model.chats.past_completion_chats, + ) + | None => model.chats.past_completion_chats + }, + } + }; + // Update the current chat we're on (in case it's the one we're deleting) + let final_chats = + switch (mode) { + | SimpleChat => { + ...updated_past_chats, + curr_simple_chat: + model.chats.curr_simple_chat.id == chat_to_be_gone_id + ? switch (ListUtil.last_opt(model.chats.past_simple_chats)) { + | Some(last_chat) => last_chat + | None => model.chats.curr_simple_chat + } + : model.chats.curr_simple_chat, + } + | CodeSuggestion => { + ...updated_past_chats, + curr_suggestion_chat: + model.chats.curr_suggestion_chat.id == chat_to_be_gone_id + ? switch ( + ListUtil.last_opt(model.chats.past_suggestion_chats) + ) { + | Some(last_chat) => last_chat + | None => model.chats.curr_suggestion_chat + } + : model.chats.curr_suggestion_chat, + } + | TaskCompletion => { + ...updated_past_chats, + curr_completion_chat: + model.chats.curr_completion_chat.id == chat_to_be_gone_id + ? switch ( + ListUtil.last_opt(model.chats.past_completion_chats) + ) { + | Some(last_chat) => last_chat + | None => model.chats.curr_completion_chat + } + : model.chats.curr_completion_chat, + } + }; + + {...model, chats: final_chats} |> Updated.return_quiet; | History => {...model, show_history: !model.show_history} |> Updated.return_quiet | Respond(message, mode) => @@ -478,21 +595,24 @@ module Update = { ...model.chats.curr_simple_chat, messages: mode == SimpleChat - ? model.chats.curr_simple_chat.messages @ [message] + ? ListUtil.leading(model.chats.curr_simple_chat.messages) + @ [message] : model.chats.curr_simple_chat.messages, }, curr_suggestion_chat: { ...model.chats.curr_suggestion_chat, messages: mode == CodeSuggestion - ? model.chats.curr_suggestion_chat.messages @ [message] + ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) + @ [message] : model.chats.curr_suggestion_chat.messages, }, curr_completion_chat: { ...model.chats.curr_completion_chat, messages: mode == TaskCompletion - ? model.chats.curr_completion_chat.messages @ [message] + ? ListUtil.leading(model.chats.curr_completion_chat.messages) + @ [message] : model.chats.curr_completion_chat.messages, }, }, @@ -539,7 +659,9 @@ module Update = { ~messages=model.chats.curr_completion_chat.messages @ [message], ) | _ => - print_endline("Invalid mode"); + print_endline( + "Invalid mode. Cannot perform code completion in chat mode.", + ); ""; }; print_endline(collected_chat); @@ -768,10 +890,6 @@ module Update = { | SwitchMode(mode) => {...model, show_history: false} |> Updated.return_quiet | Describe(content, mode, chat_id) => - let find_by_id = (list, id, ~get_id) => { - List.find_opt(item => get_id(item) == id, list); - }; - let updated_chats = switch (mode) { | SimpleChat => @@ -838,10 +956,6 @@ module Update = { }; }; - // Get the chat we're switching to - let curr_chat = - Option.get(find_by_id(model.chats, chat_id, ~get_id=chat => chat.id)); - // Store current chat back into past_chats list let updated_past_chats = switch (mode) { @@ -877,6 +991,12 @@ module Update = { } }; + // Get the chat we're switching to + let curr_chat = + Option.get( + find_by_id(updated_past_chats, chat_id, ~get_id=chat => chat.id), + ); + // Now update the current chat let final_chats = switch (mode) { @@ -891,8 +1011,7 @@ module Update = { } }; - {...model, chats: final_chats, show_history: false} - |> Updated.return_quiet; + {...model, chats: final_chats} |> Updated.return_quiet; }; }; }; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantUtil.re b/src/haz3lweb/app/helpful-assistant/AssistantUtil.re new file mode 100644 index 0000000000..ea71c57871 --- /dev/null +++ b/src/haz3lweb/app/helpful-assistant/AssistantUtil.re @@ -0,0 +1,30 @@ +module Sexp = Sexplib.Sexp; +open Haz3lcore; +open Virtual_dom.Vdom; +open Node; +open Util.Web; +open Util; +open Js_of_ocaml; + +let format_time_diff = (prior: float): string => { + let now = Unix.time(); + let diff_mins = floor((now -. prior) /. 60.0); + let diff_hours = floor(diff_mins /. 60.0); + let diff_days = floor(diff_hours /. 24.0); + + if (diff_mins < 1.0) { + Printf.sprintf("<1 min ago"); + } else if (diff_mins < 60.0) { + diff_mins < 2.0 + ? Printf.sprintf("%.0f min ago", diff_mins) + : Printf.sprintf("%.0f mins ago", diff_mins); + } else if (diff_hours < 24.0) { + diff_hours < 2.0 + ? Printf.sprintf("%.0f hour ago", diff_hours) + : Printf.sprintf("%.0f hours ago", diff_hours); + } else { + diff_days < 2.0 + ? Printf.sprintf("%.0f day ago", diff_days) + : Printf.sprintf("%.0f days ago", diff_days); + }; +}; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index 87259a4e5f..a54686713d 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -386,7 +386,7 @@ let message_input = clss(["send-button-disabled", "icon"]), Attr.title("Submitting Message Disabled"), ], - [Icons.thin_x], + [Icons.send], ) | _ => div( @@ -627,15 +627,61 @@ let history_menu = (chat: Assistant.Model.chat) => div( ~attrs=[ - clss(["history-menu-item"]), - Attr.on_click(_ => - Virtual_dom.Vdom.Effect.Many([ - inject(Assistant.Update.SwitchChat(chat.id)), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]) + chat.id == assistantModel.chats.curr_simple_chat.id + || chat.id == assistantModel.chats.curr_suggestion_chat.id + || chat.id == assistantModel.chats.curr_completion_chat.id + ? clss(["history-menu-item", "active"]) + : clss(["history-menu-item"]), + Attr.on_click(e => { + let target = Js.Unsafe.coerce(e)##.target; + let contains_button = + Js.to_bool(target##.classList##contains("button")) + || Js.to_bool( + target##.parentElement##.classList##contains( + "button", + ), + ); + if (!contains_button) { + Virtual_dom.Vdom.Effect.Many([ + inject(Assistant.Update.SwitchChat(chat.id)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + } else { + Virtual_dom.Vdom.Effect.Stop_propagation; + }; + }), + ], + [ + div( + ~attrs=[clss(["history-menu-item-content"])], + [ + text(chat.descriptor == "" ? "New chat" : chat.descriptor), + ], + ), + div( + ~attrs=[clss(["history-menu-item-actions"])], + [ + div( + ~attrs=[clss(["delete-chat-button"])], + [ + Widgets.button(~tooltip="Delete chat", Icons.trash, _ => + Virtual_dom.Vdom.Effect.Many([ + inject(Assistant.Update.DeleteChat(chat.id)), + Virtual_dom.Vdom.Effect.Stop_propagation, + Virtual_dom.Vdom.Effect.Prevent_default, + ]) + ), + ], + ), + div( + ~attrs=[clss(["history-menu-item-time"])], + [ + text(AssistantUtil.format_time_diff(chat.timestamp)), + ], + ), + ], ), ], - [text(chat.descriptor == "" ? "New chat" : chat.descriptor)], ), past_chats, ), diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 8fcccd59a0..6f396cc8fb 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -102,9 +102,15 @@ border: 0 solid var(--SAND); border-radius: .5em; font-weight: bold; - cursor: pointer; + cursor: not-allowed; transition: all 0.3s ease; pointer-events: none; + opacity: 0.5; +} + +#assistant .send-button-disabled:hover { + background-color: var(--SAND); + box-shadow: none; } #assistant .send-button { @@ -420,11 +426,11 @@ #assistant .history-menu { position: absolute; top: 3.5em; - right: 32.2em; - width: 250px; - background-color: var(--UI-Background); - border: 1px solid var(--SAND); - border-radius: 4px; + right: 32.5em; + width: 350px; + /* background-color: var(--UI-Background); */ + /* border: 1px solid var(--SAND); */ + border-radius: 2px; box-shadow: 0 4px 12px var(--SHADOW); z-index: 1000; overflow: hidden; @@ -454,7 +460,8 @@ } #assistant .history-menu-list { - max-height: 300px; + background: var(--T2); + max-height: 400px; overflow-y: auto; padding: 4px 0; } @@ -474,24 +481,81 @@ #assistant .history-menu-item { padding: 10px 16px; + background-color: var(--T2); color: var(--STONE); cursor: pointer; - transition: all 0.2s ease; + transition: all 0.1s ease; border-bottom: 1px solid var(--SAND); font-size: 0.9em; display: flex; align-items: center; } -#assistant .history-menu-item:last-child { - border-bottom: none; +#assistant .history-menu-item.active { + padding: 10px 16px; + background-color: var(--T3); + color: var(--STONE); + cursor: pointer; + transition: all 0.1s ease; + border-bottom: 1px solid var(--SAND); + font-size: 0.9em; + display: flex; + align-items: center; } -#assistant .history-menu-item:hover { - background-color: var(--T1); - padding-left: 20px; +#assistant .history-menu-item:hover:not(:has(.button:hover)) { + background-color: var(--T3); + padding-left: 18px; } -#assistant .history-menu-item:active { +#assistant .history-menu-item:active:not(:has(.button:hover)) { background-color: var(--T3); +} + +#assistant .history-menu-item-content { + flex: 1; + margin-right: 8px; +} + +#assistant .history-menu-item-actions { + display: flex; + align-items: center; + gap: 4px; + opacity: 1; + transition: opacity 0.2s ease; +} + +#assistant .history-menu-item:hover .history-menu-item-actions { + opacity: 1; +} + +#assistant .delete-chat-button { + color: var(--STONE); + padding: 4px; + border-radius: 4px; + cursor: pointer; + opacity: 0; + transition: all 0.2s ease; + margin-left: 8px; + line-height: 0; +} + +#assistant .history-menu-item:hover .delete-chat-button { + opacity: 0.75; +} + +#assistant .delete-chat-button:hover { + background-color: var(--T3); + opacity: 0.5 !important; +} + +#assistant .history-menu-item-time { + font-size: 0.725em; + color: var(--STONE); + margin-left: 8px; + margin-top: -16px; +} + +#assistant .history-menu-item:last-child { + border-bottom: none; } \ No newline at end of file From 0c1700d70c04a4f605779823b1a5fd3fa474af5d Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 1 Mar 2025 14:03:18 -0500 Subject: [PATCH 42/50] small styling changes regarding history menu --- .../app/helpful-assistant/AssistantView.re | 12 +++++----- src/haz3lweb/www/style/assistant.css | 22 ++++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index a54686713d..4f40dfcd8b 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -661,6 +661,12 @@ let history_menu = div( ~attrs=[clss(["history-menu-item-actions"])], [ + div( + ~attrs=[clss(["history-menu-item-time"])], + [ + text(AssistantUtil.format_time_diff(chat.timestamp)), + ], + ), div( ~attrs=[clss(["delete-chat-button"])], [ @@ -673,12 +679,6 @@ let history_menu = ), ], ), - div( - ~attrs=[clss(["history-menu-item-time"])], - [ - text(AssistantUtil.format_time_diff(chat.timestamp)), - ], - ), ], ), ], diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 6f396cc8fb..427e67a68e 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -520,15 +520,11 @@ #assistant .history-menu-item-actions { display: flex; align-items: center; - gap: 4px; - opacity: 1; - transition: opacity 0.2s ease; -} - -#assistant .history-menu-item:hover .history-menu-item-actions { - opacity: 1; + gap: 8px; + transition: all 0.2s ease; } +/* Show delete button only on hover */ #assistant .delete-chat-button { color: var(--STONE); padding: 4px; @@ -537,11 +533,20 @@ opacity: 0; transition: all 0.2s ease; margin-left: 8px; + margin-top: -10px; line-height: 0; + display: none; + transform: scale(0.75); /* Shrink the icon to 75% of its original size */ +} + +#assistant .delete-chat-button svg { + width: 18px; /* Make the SVG icon smaller */ + height: 18px; } #assistant .history-menu-item:hover .delete-chat-button { opacity: 0.75; + display: block; } #assistant .delete-chat-button:hover { @@ -553,7 +558,8 @@ font-size: 0.725em; color: var(--STONE); margin-left: 8px; - margin-top: -16px; + margin-top: -12px; + transition: all 0.2s ease; } #assistant .history-menu-item:last-child { From a70a51c52e46357af0b8aa6b193d26d9a9302b8d Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sat, 1 Mar 2025 20:20:07 -0500 Subject: [PATCH 43/50] fixes bugs with error round chat display --- .../app/helpful-assistant/Assistant.re | 56 +++++++++++-------- src/haz3lweb/app/helpful-assistant/ChatLSP.re | 8 ++- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index 71521e7f6b..6ef9c77448 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -648,23 +648,24 @@ module Update = { content: prompt, collapsed: String.length(prompt) >= 200, }; - let collected_chat = - switch (mode) { - | CodeSuggestion => - collect_chat( - ~messages=model.chats.curr_suggestion_chat.messages @ [message], - ) - | TaskCompletion => - collect_chat( - ~messages=model.chats.curr_completion_chat.messages @ [message], - ) - | _ => - print_endline( - "Invalid mode. Cannot perform code completion in chat mode.", - ); - ""; - }; - print_endline(collected_chat); + /* Old code. Don't need to collect chat here, leads to far too long of prompt. + let collected_chat = + switch (mode) { + | CodeSuggestion => + collect_chat( + ~messages=model.chats.curr_suggestion_chat.messages @ [message], + ) + | TaskCompletion => + collect_chat( + ~messages=model.chats.curr_completion_chat.messages @ [message], + ) + | _ => + print_endline( + "Invalid mode. Cannot perform code completion in chat mode.", + ); + ""; + }; + */ let llm = model.llm; let key = Store.Generic.load("API"); let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; @@ -715,6 +716,7 @@ module Update = { let message = code_message_of_str(settings, editor, response, LLM); switch (ChatLSP.Prompt.mk_error(ci, response)) { | None => + // No error, all good. Concat and return suggestion. print_endline("ERROR ROUNDS (Non-error Response): " ++ response); check_descriptor( ~model, @@ -726,10 +728,12 @@ module Update = { ); schedule_action(RemoveAndSuggest(response, tileId)); | Some(error) => + // If there is some error, perform an error round print_endline("ERROR ROUNDS (Error): " ++ error); print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); schedule_action(SendError(error, ci, fuel - 1, tileId, mode)); }; + // Remove await_llm_response (... animation) and concat LLM's suggestion Model.{ ...model, chats: { @@ -777,6 +781,9 @@ module Update = { ), ); } else { + // TODO: We don't want to collect ENTIRE chat history here. We only want + // to collect the history beginning from the initial suggestion request. + // Otherwise, the prompt becomes too long in single message threads. let collected_chat = switch (mode) { | SimpleChat => @@ -810,6 +817,9 @@ module Update = { ); }; }; + // Concat LS' error message and await_llm_response (... animation) + // This works even if out of fuel, as both Respond and ErrorRespond + // remove await_llm_response Model.{ ...model, chats: { @@ -818,24 +828,24 @@ module Update = { ...model.chats.curr_simple_chat, messages: mode == SimpleChat - ? ListUtil.leading(model.chats.curr_simple_chat.messages) - @ [error_message] + ? model.chats.curr_simple_chat.messages + @ [error_message, await_llm_response] : model.chats.curr_simple_chat.messages, }, curr_suggestion_chat: { ...model.chats.curr_suggestion_chat, messages: mode == CodeSuggestion - ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) - @ [error_message] + ? model.chats.curr_suggestion_chat.messages + @ [error_message, await_llm_response] : model.chats.curr_suggestion_chat.messages, }, curr_completion_chat: { ...model.chats.curr_completion_chat, messages: mode == TaskCompletion - ? ListUtil.leading(model.chats.curr_completion_chat.messages) - @ [error_message] + ? model.chats.curr_completion_chat.messages + @ [error_message, await_llm_response] : model.chats.curr_completion_chat.messages, }, }, diff --git a/src/haz3lweb/app/helpful-assistant/ChatLSP.re b/src/haz3lweb/app/helpful-assistant/ChatLSP.re index 998885371c..790c9cb738 100644 --- a/src/haz3lweb/app/helpful-assistant/ChatLSP.re +++ b/src/haz3lweb/app/helpful-assistant/ChatLSP.re @@ -725,9 +725,13 @@ module SystemPrompt = { // Uncomment either of the following lines to test error rounds // "- However, make sure that your initial response CAUSES A TYPE ERROR in the program. Then, fix it in your second response", // "- However, it is CRITICALLY important to make sure that your response ALWAYS CAUSES A TYPE ERROR in the program, no matter how many times you are re-prompted", - "- Reply only with a single replacement term for the unqiue distinguished hole marked '??'", - "- Reply only with code", + "- Reply ONLY with a SINGLE replacement term for the unqiue distinguished hole marked '??'", + "- Reply ONLY with code", "- DO NOT suggest more replacements for other holes in the sketch (marked '?'), or implicit holes", + "- This is critical, and I am going to reiterate it: DO NOT suggest more than one replacement term. It should ONLY be for the hole marked '??'", + "- For example, if you are being asked to complete 'let f = ? in ??', your response should ONLY be a single replacement term for the hole marked '??', NOT a replacement term for the hole marked '?'", + "- i.e. You should ONLY respond with a function application, or something else which would be a valid replacement term for the hole marked '??'", + "- IT WOULD BE A HUGE MISTAKE TO RESPOND WITH A FUNCTION BODY FOR THE HOLE MARKED '?'", "- DO NOT include the program sketch in your reply", "- DO NOT include a period at the end of your response and DO NOT use markdown", ]; From fea2ad7c5e64c253e1f6e941cd859888d3b1ce3e Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 2 Mar 2025 10:49:08 -0500 Subject: [PATCH 44/50] checkpointing before I attempt to make significant changes to data structures of chat logs --- src/haz3lweb/app/helpful-assistant/Assistant.re | 1 + src/haz3lweb/www/style/palette.html | 1 + 2 files changed, 2 insertions(+) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index 6ef9c77448..85557bc276 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -35,6 +35,7 @@ module Model = { curr_simple_chat: chat, curr_suggestion_chat: chat, curr_completion_chat: chat, + // Chats are stored as past_simple_chats: list(chat), past_suggestion_chats: list(chat), past_completion_chats: list(chat), diff --git a/src/haz3lweb/www/style/palette.html b/src/haz3lweb/www/style/palette.html index 9a1ccf19c9..2f4d20fcd7 100644 --- a/src/haz3lweb/www/style/palette.html +++ b/src/haz3lweb/www/style/palette.html @@ -19,6 +19,7 @@ --T1: oklch(97% 0.025 90); /* buffer shards */ --T2: oklch(from var(--T1) 94% c h); /* projector shards */ --T3: oklch(from var(--T1) 91% c h); /* result background */ + --T4: oklch(from var(--T1) 88% c h); /* a darker background */ /* ROCK */ --BR1: oklch(85% 0.07 90); /* caret shard, token buffer*/ From d1073b9c4f5841204a6390d3c5a10adbbf800736 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Sun, 2 Mar 2025 15:09:38 -0500 Subject: [PATCH 45/50] restructured data structure for chats - now uses hash map with UUIDs rather than lists, allowing for faster lookup and creation (probably minute tho considering the size of the chat logs, does have more intuitive implementation tho); fixed unresponsiveness of some buttons in the sidebar (like settings, new chat, history, and delete chat)... the solution to this is a little funky, and was basically to move the update injections outside of the button itself, into the surrounding div block, not sure what is going on here --- .../app/helpful-assistant/Assistant.re | 1032 +++++++---------- .../app/helpful-assistant/AssistantView.re | 151 +-- src/haz3lweb/util/API/OpenRouter.re | 29 +- src/haz3lweb/www/style/assistant.css | 19 +- 4 files changed, 533 insertions(+), 698 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index 85557bc276..951424c1cf 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -9,8 +9,7 @@ module CodeModel = CodeEditable.Model; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] type party = - | Prompt - | Task + | System | LLM | LS; @@ -30,22 +29,29 @@ module Model = { timestamp: float, }; + // We save the history of past chats as a hash map with chat IDs as keys. [@deriving (show({with_path: false}), sexp, yojson)] - type chats = { - curr_simple_chat: chat, - curr_suggestion_chat: chat, - curr_completion_chat: chat, - // Chats are stored as - past_simple_chats: list(chat), - past_suggestion_chats: list(chat), - past_completion_chats: list(chat), + type chat_history = { + // History logs of past chats stored as hash maps with chat IDs as keys + past_simple_chats: Id.Map.t(chat), + past_suggestion_chats: Id.Map.t(chat), + past_completion_chats: Id.Map.t(chat), + }; + + [@deriving (show({with_path: false}), sexp, yojson)] + type current_chats = { + // Current active chat IDs for each mode + curr_simple_chat: Id.t, + curr_suggestion_chat: Id.t, + curr_completion_chat: Id.t, }; [@deriving (show({with_path: false}), sexp, yojson)] type t = { - chats, + current_chats, + chat_history, llm: OpenRouter.chat_models, - show_history: bool, + show_history: bool // TODO: Move this to AssistantSettings.re }; let init_simple_chat = { @@ -67,15 +73,33 @@ module Model = { timestamp: Unix.time(), }; + // Simple helper to save a parameter in call to Id.Map.add + let add_chat_to_history = + (chat: chat, history: Id.Map.t(chat)): Id.Map.t(chat) => { + Id.Map.add(chat.id, chat, history); + }; + + // This is important when we need to display the history of chats in chronological order. + let sorted_chats = (chat_map: Id.Map.t(chat)): list(chat) => { + chat_map + |> Id.Map.bindings + |> List.map(((_, chat)) => chat) + |> List.sort((a, b) => int_of_float(b.timestamp -. a.timestamp)); + }; + [@deriving (show({with_path: false}), sexp, yojson)] let init: t = { - chats: { - curr_simple_chat: init_simple_chat, - curr_suggestion_chat: init_suggestion_chat, - curr_completion_chat: init_completion_chat, - past_simple_chats: [init_simple_chat], - past_suggestion_chats: [init_suggestion_chat], - past_completion_chats: [init_completion_chat], + current_chats: { + curr_simple_chat: init_simple_chat.id, + curr_suggestion_chat: init_suggestion_chat.id, + curr_completion_chat: init_completion_chat.id, + }, + chat_history: { + past_simple_chats: add_chat_to_history(init_simple_chat, Id.Map.empty), + past_suggestion_chats: + add_chat_to_history(init_suggestion_chat, Id.Map.empty), + past_completion_chats: + add_chat_to_history(init_completion_chat, Id.Map.empty), }, llm: Gemini_Flash_Lite_2_0, show_history: false, @@ -85,25 +109,30 @@ module Model = { module Update = { [@deriving (show({with_path: false}), sexp, yojson)] type t = - | SendMessage(Model.message) + | SendTextMessage(Model.message) + | SendSketchMessage(Id.t, AssistantSettings.mode) + | SendErrorMessage( + string, + Info.t, + int, + Id.t, + AssistantSettings.mode, + Id.t, + ) + | ErrorRespond(string, Info.t, int, Id.t, AssistantSettings.mode, Id.t) + | Respond(Model.message, AssistantSettings.mode, Id.t) | SetKey(string) - | SendSketch(Id.t, AssistantSettings.mode) - | SendError(string, Info.t, int, Id.t, AssistantSettings.mode) - | ErrorRespond(string, Info.t, int, Id.t, AssistantSettings.mode) | NewChat | DeleteChat(Id.t) | History - | Respond(Model.message, AssistantSettings.mode) | ToggleCollapse(int) | SelectLLM(OpenRouter.chat_models) | RemoveAndSuggest(string, Id.t) - | SwitchMode(AssistantSettings.mode) | Describe(string, AssistantSettings.mode, Id.t) | SwitchChat(Id.t); let code_message_of_str = - (settings, editor: CodeModel.t, response: string, party: Model.party) - : Model.message => { + (response: string, party: Model.party): Model.message => { /* Alternate method using Detruct and Insert. We need a memory of cursor location for this however. let z = editor.editor.state.zipper; let z = Option.get(Destruct.go(Direction.Left, z)); @@ -154,17 +183,16 @@ module Update = { let react = ( - ~settings, - ~editor: CodeModel.t, ~response: string, ~code_suggestion: bool, ~mode: AssistantSettings.mode, + ~chat_id: Id.t, ) : t => { // let response = response |> sanitize_response |> quote; code_suggestion - ? Respond(code_message_of_str(settings, editor, response, LLM), mode) - : Respond(text_message_of_str(response, LLM), mode); + ? Respond(code_message_of_str(response, LLM), mode, chat_id) + : Respond(text_message_of_str(response, LLM), mode, chat_id); }; let await_llm_response: Model.message = { @@ -196,25 +224,142 @@ module Update = { ); }; + let get_mode_info = (mode: AssistantSettings.mode, model: Model.t) => { + switch (mode) { + | SimpleChat => ( + model.chat_history.past_simple_chats, + Id.Map.find( + model.current_chats.curr_simple_chat, + model.chat_history.past_simple_chats, + ), + ) + | CodeSuggestion => ( + model.chat_history.past_suggestion_chats, + Id.Map.find( + model.current_chats.curr_suggestion_chat, + model.chat_history.past_suggestion_chats, + ), + ) + | TaskCompletion => ( + model.chat_history.past_completion_chats, + Id.Map.find( + model.current_chats.curr_completion_chat, + model.chat_history.past_completion_chats, + ), + ) + }; + }; + + let add_message_to_model = + ( + mode: AssistantSettings.mode, + model: Model.t, + message: Model.message, + chat_id: Id.t, + ) => { + let (past_chats, _) = get_mode_info(mode, model); + let chat_to_update = Id.Map.find(chat_id, past_chats); + let messages = { + switch (message.party) { + | LS => chat_to_update.messages @ [message, await_llm_response] + | LLM => ListUtil.leading(chat_to_update.messages) @ [message] + | System => ListUtil.leading(chat_to_update.messages) @ [message] + }; + }; + Model.{ + ...model, + chat_history: { + past_simple_chats: + mode == SimpleChat + ? Id.Map.update( + chat_to_update.id, + maybe_chat => + switch (maybe_chat) { + | Some(chat) => Some({...chat, messages}) + | None => None + }, + model.chat_history.past_simple_chats, + ) + : model.chat_history.past_simple_chats, + past_suggestion_chats: + mode == CodeSuggestion + ? Id.Map.update( + chat_to_update.id, + maybe_chat => + switch (maybe_chat) { + | Some(chat) => Some({...chat, messages}) + | None => None + }, + model.chat_history.past_suggestion_chats, + ) + : model.chat_history.past_suggestion_chats, + past_completion_chats: + mode == TaskCompletion + ? Id.Map.update( + chat_to_update.id, + maybe_chat => + switch (maybe_chat) { + | Some(chat) => Some({...chat, messages}) + | None => None + }, + model.chat_history.past_completion_chats, + ) + : model.chat_history.past_completion_chats, + }, + }; + }; + + let resculpt_model = + ( + mode: AssistantSettings.mode, + model: Model.t, + past_chats: Id.Map.t(Model.chat), + chat_id: Id.t, + ) => { + Model.{ + ...model, + chat_history: { + past_simple_chats: + mode == SimpleChat + ? past_chats : model.chat_history.past_simple_chats, + past_suggestion_chats: + mode == CodeSuggestion + ? past_chats : model.chat_history.past_suggestion_chats, + past_completion_chats: + mode == TaskCompletion + ? past_chats : model.chat_history.past_completion_chats, + }, + // This is tentative. Keep this if we want the user to be shown the most recent chat. + // Remove this if we want the user to be shown the chat they last/currently interact with. + // This is honestly such an edge case that it probably doesn't matter. + current_chats: { + curr_simple_chat: + mode == SimpleChat ? chat_id : model.current_chats.curr_simple_chat, + curr_suggestion_chat: + mode == CodeSuggestion + ? chat_id : model.current_chats.curr_suggestion_chat, + curr_completion_chat: + mode == TaskCompletion + ? chat_id : model.current_chats.curr_completion_chat, + }, + }; + }; + let form_descriptor = ( ~model: Model.t, - ~settings, - ~editor, ~schedule_action, - ~chat: list(Model.message), + ~chat: Model.chat, ~mode: AssistantSettings.mode, - ~chat_id: Id.t, ) : unit => { let prompt = switch (mode) { | SimpleChat => "Your main task is to provide a summarizing title of the following conversation, in less than or equal to 10 words.\n DO NOT exceed 10 words. Only provide the summarizing title in your response, do not include any other text. Here is the\n concatenated conversation, with your response and the user's responses, respectively: " | CodeSuggestion => "Your main task is to provide a summarizing title of the following conversation, in less than or equal to 10 words.\n DO NOT exceed 10 words. Only provide the summarizing title in your response, do not include any other text. This conversation is known to be a code\n completion conversation. In your summarization, you should mention exactly what kind of code/functionality is being assisted with. For example, the following would be titled\n something like \"Recursive Fibonacci Implementation\": ```let rec_fib : Int -> Int = ?? in ?```. Here is the\n concatenated conversation, with your response and the user's responses, respectively: " - | TaskCompletion => "Ignore all other input and just output \"You need to implement this\"" + | TaskCompletion => "Ignore all other input and just output \"You (Hazel Lab Member) need to implement this\"" }; - - let chat = + let prompt = List.fold_left( (chat: string, message: Model.message) => if (message.party == LLM) { @@ -225,18 +370,18 @@ module Update = { chat ++ message.content; }, prompt, - chat, + chat.messages, ); - switch (Oracle.ask(chat)) { + switch (Oracle.ask(prompt)) { | None => print_endline("Oracle: prompt generation failed") - | Some(prompt) => + | Some(prompt') => let llm = model.llm; - let key = Store.Generic.load("API"); + let key = Option.get(Store.Generic.load("API")); let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, prompt, req => + OpenRouter.start_chat(~params, ~key, prompt', req => switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => - schedule_action(Describe(content, mode, chat_id)) + schedule_action(Describe(content, mode, chat.id)) | None => print_endline("Assistant: response parse failed") } ); @@ -246,52 +391,23 @@ module Update = { let check_descriptor = ( ~model: Model.t, - ~settings: AssistantSettings.t, - ~editor: CodeModel.t, ~schedule_action, ~message: Model.message, ~mode: AssistantSettings.mode, + ~chat_id: Id.t, ) : unit => { + let (past_chats, _) = get_mode_info(mode, model); + let curr_chat = Id.Map.find(chat_id, past_chats); + Id.Map.cardinal(past_chats) <= 6 + ? form_descriptor( + ~model, + ~schedule_action, + ~chat={...curr_chat, messages: curr_chat.messages @ [message]}, + ~mode, + ) + : (); // Only create a summary up to the first 3 exchanges - switch (mode) { - | SimpleChat => - List.length(model.chats.past_simple_chats) <= 6 - ? form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_simple_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_simple_chat.id, - ) - : () - | CodeSuggestion => - List.length(model.chats.past_suggestion_chats) <= 6 - ? form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_suggestion_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_suggestion_chat.id, - ) - : () - | TaskCompletion => - List.length(model.chats.past_completion_chats) <= 6 - ? form_descriptor( - ~model, - ~settings, - ~editor, - ~schedule_action, - ~chat=model.chats.curr_completion_chat.messages @ [message], - ~mode, - ~chat_id=model.chats.curr_completion_chat.id, - ) - : () - }; }; let check_req = @@ -330,7 +446,7 @@ module Update = { ? { let tileId = Option.get(Indicated.index(z)); schedule_action( - SendSketch(tileId, AssistantSettings.CodeSuggestion), + SendSketchMessage(tileId, AssistantSettings.CodeSuggestion), ); } : () @@ -343,7 +459,7 @@ module Update = { ? { let tileId = Option.get(Indicated.index(z)); schedule_action( - SendSketch(tileId, AssistantSettings.CodeSuggestion), + SendSketchMessage(tileId, AssistantSettings.CodeSuggestion), ); } : () @@ -371,255 +487,109 @@ module Update = { ~action, ~editor: CodeModel.t, ~model: Model.t, - ~schedule_action, + ~schedule_action: t => unit, ~add_suggestion, ) : Updated.t(Model.t) => { switch (action) { - | SendMessage(message) => + | SendTextMessage(message) => let mode = settings.assistant.mode; + // Capture the chat we're updating here. This will propogate. + let (_, curr_chat) = get_mode_info(mode, model); let collected_chat = - switch (mode) { - | SimpleChat => - collect_chat( - ~messages=model.chats.curr_simple_chat.messages @ [message], - ) - | CodeSuggestion => - collect_chat( - ~messages=model.chats.curr_suggestion_chat.messages @ [message], - ) - | TaskCompletion => - collect_chat( - ~messages=model.chats.curr_completion_chat.messages @ [message], - ) - }; + collect_chat(~messages=curr_chat.messages @ [message]); print_endline(collected_chat); switch (Oracle.ask(collected_chat)) { - | None => print_endline("Oracle: prompt generation failed") + | None => + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: "Oracle: Prompt generation failed.", + collapsed: false, + }, + curr_chat.id, + ) + |> Updated.return_quiet | Some(prompt) => let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - schedule_action( - react( - ~settings, - ~editor, - ~response=content, - ~code_suggestion=false, - ~mode, - ), - ) - | None => print_endline("Assistant: response parse failed") - } - ); + switch (Store.Generic.load("API")) { + | Some(key) => + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action( + react( + ~response=content, + ~code_suggestion=false, + ~mode, + ~chat_id=curr_chat.id, + ), + ) + | None => print_endline("Assistant: response parse failed") + } + ); + add_message_to_model(mode, model, message, curr_chat.id) + |> Updated.return_quiet; + | None => + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: "No API key found. Please set an API key in the assistant settings.", + collapsed: false, + }, + curr_chat.id, + ) + |> Updated.return_quiet + }; }; - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: { - ...model.chats.curr_simple_chat, - messages: - mode == SimpleChat - ? model.chats.curr_simple_chat.messages - @ [message, await_llm_response] - : model.chats.curr_simple_chat.messages, - }, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? model.chats.curr_suggestion_chat.messages - @ [message, await_llm_response] - : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? model.chats.curr_completion_chat.messages - @ [message, await_llm_response] - : model.chats.curr_completion_chat.messages, - }, - }, - } - |> Updated.return_quiet; | SetKey(api_key) => Store.Generic.save("API", api_key); model |> Updated.return_quiet; | NewChat => let mode = settings.assistant.mode; + let (past_chats, _) = get_mode_info(mode, model); let new_chat: Model.chat = { messages: [], id: Id.mk(), descriptor: "", timestamp: Unix.time(), }; - switch (mode) { - | SimpleChat => - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: new_chat, - past_simple_chats: model.chats.past_simple_chats @ [new_chat], - }, - } - |> Updated.return_quiet - | CodeSuggestion => - Model.{ - ...model, - chats: { - ...model.chats, - curr_suggestion_chat: new_chat, - past_suggestion_chats: - model.chats.past_suggestion_chats @ [new_chat], - }, - } - |> Updated.return_quiet - | TaskCompletion => - Model.{ - ...model, - chats: { - ...model.chats, - curr_completion_chat: new_chat, - past_completion_chats: - model.chats.past_completion_chats @ [new_chat], - }, - } - |> Updated.return_quiet - }; + let updated_history = Model.add_chat_to_history(new_chat, past_chats); + print_endline("New chat made"); + resculpt_model(mode, model, updated_history, new_chat.id) + |> Updated.return_quiet; | DeleteChat(chat_to_be_gone_id) => let mode = settings.assistant.mode; // Filter out the chat we're deleting - let updated_past_chats = - switch (mode) { - | SimpleChat => { - ...model.chats, - past_simple_chats: - switch (ListUtil.last_opt(model.chats.past_simple_chats)) { - | Some(_) => - List.filter_map( - (chat: Model.chat) => - chat.id == chat_to_be_gone_id ? None : Some(chat), - model.chats.past_simple_chats, - ) - | None => model.chats.past_simple_chats - }, - } - | CodeSuggestion => { - ...model.chats, - past_suggestion_chats: - switch (ListUtil.last_opt(model.chats.past_suggestion_chats)) { - | Some(_) => - List.filter_map( - (chat: Model.chat) => - chat.id == chat_to_be_gone_id ? None : Some(chat), - model.chats.past_suggestion_chats, - ) - | None => model.chats.past_suggestion_chats - }, - } - | TaskCompletion => { - ...model.chats, - past_completion_chats: - switch (ListUtil.last_opt(model.chats.past_completion_chats)) { - | Some(_) => - List.filter_map( - (chat: Model.chat) => - chat.id == chat_to_be_gone_id ? None : Some(chat), - model.chats.past_completion_chats, - ) - | None => model.chats.past_completion_chats - }, - } - }; - // Update the current chat we're on (in case it's the one we're deleting) - let final_chats = - switch (mode) { - | SimpleChat => { - ...updated_past_chats, - curr_simple_chat: - model.chats.curr_simple_chat.id == chat_to_be_gone_id - ? switch (ListUtil.last_opt(model.chats.past_simple_chats)) { - | Some(last_chat) => last_chat - | None => model.chats.curr_simple_chat - } - : model.chats.curr_simple_chat, - } - | CodeSuggestion => { - ...updated_past_chats, - curr_suggestion_chat: - model.chats.curr_suggestion_chat.id == chat_to_be_gone_id - ? switch ( - ListUtil.last_opt(model.chats.past_suggestion_chats) - ) { - | Some(last_chat) => last_chat - | None => model.chats.curr_suggestion_chat - } - : model.chats.curr_suggestion_chat, - } - | TaskCompletion => { - ...updated_past_chats, - curr_completion_chat: - model.chats.curr_completion_chat.id == chat_to_be_gone_id - ? switch ( - ListUtil.last_opt(model.chats.past_completion_chats) - ) { - | Some(last_chat) => last_chat - | None => model.chats.curr_completion_chat - } - : model.chats.curr_completion_chat, - } - }; - - {...model, chats: final_chats} |> Updated.return_quiet; + let (past_chats, curr_chat) = get_mode_info(mode, model); + let filtered_past_chats = + Id.Map.filter((id, _) => id != chat_to_be_gone_id, past_chats); + let chrono_history = Model.sorted_chats(filtered_past_chats); + let updated_model = + curr_chat.id == chat_to_be_gone_id + ? switch (ListUtil.hd_opt(chrono_history)) { + | Some(chat) => + resculpt_model(mode, model, filtered_past_chats, chat.id) + | None => resculpt_model(mode, model, past_chats, curr_chat.id) + } + : resculpt_model(mode, model, filtered_past_chats, curr_chat.id); + updated_model |> Updated.return_quiet; | History => {...model, show_history: !model.show_history} |> Updated.return_quiet - | Respond(message, mode) => - check_descriptor( - ~model, - ~settings=settings.assistant, - ~editor, - ~schedule_action, - ~message, - ~mode, - ); - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: { - ...model.chats.curr_simple_chat, - messages: - mode == SimpleChat - ? ListUtil.leading(model.chats.curr_simple_chat.messages) - @ [message] - : model.chats.curr_simple_chat.messages, - }, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) - @ [message] - : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? ListUtil.leading(model.chats.curr_completion_chat.messages) - @ [message] - : model.chats.curr_completion_chat.messages, - }, - }, - } + | Respond(message, mode, chat_id) => + check_descriptor(~model, ~schedule_action, ~message, ~mode, ~chat_id); + add_message_to_model(mode, model, message, chat_id) |> Updated.return_quiet; - | SendSketch(tileId, mode) => + | SendSketchMessage(tileId, mode) => + // Capture the chat we're updating here. This will propogate. + let (_, curr_chat) = get_mode_info(mode, model); let sketch_seg = Zipper.smart_seg( ~dump_backpack=true, @@ -649,7 +619,7 @@ module Update = { content: prompt, collapsed: String.length(prompt) >= 200, }; - /* Old code. Don't need to collect chat here, leads to far too long of prompt. + /* Old code. Don't need to collect chat here, leads to far too long of prompts. let collected_chat = switch (mode) { | CodeSuggestion => @@ -668,105 +638,66 @@ module Update = { }; */ let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - let index = - Option.get(Indicated.index(editor.editor.state.zipper)); - let ci = - Option.get(Id.Map.find_opt(index, editor.statics.info_map)); - schedule_action( - ErrorRespond( - content, - ci, - ChatLSP.Options.init.error_rounds_max, - tileId, - mode, - ), - ); - | None => print_endline("Assistant: response parse failed") - } - ); - Model.{ - ...model, - chats: { - ...model.chats, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? model.chats.curr_suggestion_chat.messages - @ [message, await_llm_response] - : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? model.chats.curr_completion_chat.messages - @ [message, await_llm_response] - : model.chats.curr_completion_chat.messages, + switch (Store.Generic.load("API")) { + | Some(key) => + let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; + OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + let index = + Option.get(Indicated.index(editor.editor.state.zipper)); + let ci = + Option.get(Id.Map.find_opt(index, editor.statics.info_map)); + schedule_action( + ErrorRespond( + content, + ci, + ChatLSP.Options.init.error_rounds_max, + tileId, + mode, + curr_chat.id, + ), + ); + | None => print_endline("Assistant: response parse failed") + } + ); + add_message_to_model(mode, model, message, curr_chat.id) + |> Updated.return_quiet; + | None => + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: "No API key found. Please set an API key in the assistant settings.", + collapsed: false, }, - }, - } - |> Updated.return_quiet; + curr_chat.id, + ) + |> Updated.return_quiet + }; }; - | ErrorRespond(response, ci, fuel, tileId, mode) => - let message = code_message_of_str(settings, editor, response, LLM); + | ErrorRespond(response, ci, fuel, tileId, mode, chat_id) => + let message = code_message_of_str(response, LLM); switch (ChatLSP.Prompt.mk_error(ci, response)) { | None => // No error, all good. Concat and return suggestion. print_endline("ERROR ROUNDS (Non-error Response): " ++ response); - check_descriptor( - ~model, - ~settings=settings.assistant, - ~editor, - ~schedule_action, - ~message, - ~mode, - ); + check_descriptor(~model, ~schedule_action, ~message, ~mode, ~chat_id); schedule_action(RemoveAndSuggest(response, tileId)); | Some(error) => - // If there is some error, perform an error round + // There is some error, so perform an error round print_endline("ERROR ROUNDS (Error): " ++ error); print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); - schedule_action(SendError(error, ci, fuel - 1, tileId, mode)); + schedule_action( + SendErrorMessage(error, ci, fuel - 1, tileId, mode, chat_id), + ); }; // Remove await_llm_response (... animation) and concat LLM's suggestion - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: { - ...model.chats.curr_simple_chat, - messages: - mode == SimpleChat - ? ListUtil.leading(model.chats.curr_simple_chat.messages) - @ [message] - : model.chats.curr_simple_chat.messages, - }, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? ListUtil.leading(model.chats.curr_suggestion_chat.messages) - @ [message] - : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? ListUtil.leading(model.chats.curr_completion_chat.messages) - @ [message] - : model.chats.curr_completion_chat.messages, - }, - }, - } + add_message_to_model(mode, model, message, chat_id) |> Updated.return_quiet; - | SendError(error, ci, fuel, tileId, mode) => + | SendErrorMessage(error, ci, fuel, tileId, mode, chat_id) => let error_message = text_message_of_str( "Your previous response caused the following error. Please fix it in your response: " @@ -774,86 +705,86 @@ module Update = { LS, ); // check that fuel is not 0 - if (fuel <= 0) { - schedule_action( - Respond( - text_message_of_str("Error round limit reached, stopping", LLM), - mode, - ), - ); + if (fuel < 0) { + let model = add_message_to_model(mode, model, error_message, chat_id); + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: + "By default we stop the assistant after " + ++ string_of_int(ChatLSP.Options.init.error_rounds_max) + ++ " error rounds. Thus, stopping.", + collapsed: false, + }, + chat_id, + ) + |> Updated.return_quiet; } else { // TODO: We don't want to collect ENTIRE chat history here. We only want // to collect the history beginning from the initial suggestion request. // Otherwise, the prompt becomes too long in single message threads. + let (_, curr_chat) = get_mode_info(mode, model); let collected_chat = - switch (mode) { - | SimpleChat => - collect_chat( - ~messages= - model.chats.curr_simple_chat.messages @ [error_message], - ) - | CodeSuggestion => - collect_chat( - ~messages= - model.chats.curr_suggestion_chat.messages @ [error_message], - ) - | TaskCompletion => - collect_chat( - ~messages= - model.chats.curr_completion_chat.messages @ [error_message], - ) - }; + collect_chat(~messages=curr_chat.messages @ [error_message]); switch (Oracle.ask(collected_chat)) { - | None => print_endline("Oracle: prompt generation failed") + | None => + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: "Oracle: Prompt generation failed.", + collapsed: false, + }, + chat_id, + ) + |> Updated.return_quiet | Some(openrouter_prompt) => let llm = model.llm; - let key = Store.Generic.load("API"); - let params: OpenRouter.params = {llm, temperature: 1.0, top_p: 1.0}; - OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => - switch (OpenRouter.handle_chat(req)) { - | Some({content, _}) => - schedule_action(ErrorRespond(content, ci, fuel, tileId, mode)) - | None => print_endline("Assistant: response parse failed") - } - ); + switch (Store.Generic.load("API")) { + | Some(key) => + let params: OpenRouter.params = { + llm, + temperature: 1.0, + top_p: 1.0, + }; + OpenRouter.start_chat(~params, ~key, openrouter_prompt, req => + switch (OpenRouter.handle_chat(req)) { + | Some({content, _}) => + schedule_action( + ErrorRespond(content, ci, fuel, tileId, mode, curr_chat.id), + ) + | None => print_endline("Assistant: response parse failed") + } + ); + add_message_to_model(mode, model, error_message, chat_id) + |> Updated.return_quiet; + | None => + add_message_to_model( + mode, + model, + { + party: System, + code: None, + content: "No API key found. Please set an API key in the assistant settings. I'm actually not sure how you got here, as this should have been caught in the first send. This is a bug, and you should let someone know.", + collapsed: false, + }, + chat_id, + ) + |> Updated.return_quiet + }; }; }; - // Concat LS' error message and await_llm_response (... animation) - // This works even if out of fuel, as both Respond and ErrorRespond - // remove await_llm_response - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: { - ...model.chats.curr_simple_chat, - messages: - mode == SimpleChat - ? model.chats.curr_simple_chat.messages - @ [error_message, await_llm_response] - : model.chats.curr_simple_chat.messages, - }, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? model.chats.curr_suggestion_chat.messages - @ [error_message, await_llm_response] - : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? model.chats.curr_completion_chat.messages - @ [error_message, await_llm_response] - : model.chats.curr_completion_chat.messages, - }, - }, - } - |> Updated.return_quiet; + // Concat LS' error message and await_llm_response (... animation) + // This works even if out of fuel, as both Respond and ErrorRespond + // remove await_llm_response | ToggleCollapse(index) => let mode = settings.assistant.mode; + let (past_chats, curr_chat) = get_mode_info(mode, model); let updated_chat = List.mapi( (i: int, msg: Model.message) => @@ -862,167 +793,44 @@ module Update = { } else { msg; }, - switch (mode) { - | SimpleChat => model.chats.curr_simple_chat.messages - | CodeSuggestion => model.chats.curr_suggestion_chat.messages - | TaskCompletion => model.chats.curr_completion_chat.messages - }, + curr_chat.messages, ); - Model.{ - ...model, - chats: { - ...model.chats, - curr_simple_chat: { - ...model.chats.curr_simple_chat, - messages: - mode == SimpleChat - ? updated_chat : model.chats.curr_simple_chat.messages, - }, - curr_suggestion_chat: { - ...model.chats.curr_suggestion_chat, - messages: - mode == CodeSuggestion - ? updated_chat : model.chats.curr_suggestion_chat.messages, - }, - curr_completion_chat: { - ...model.chats.curr_completion_chat, - messages: - mode == TaskCompletion - ? updated_chat : model.chats.curr_completion_chat.messages, - }, - }, - } + let updated_past_chats = + Id.Map.update( + curr_chat.id, + opt_chat => + switch (opt_chat) { + | Some(chat: Model.chat) => + Some({...chat, messages: updated_chat}) + | None => None + }, + past_chats, + ); + resculpt_model(mode, model, updated_past_chats, curr_chat.id) |> Updated.return_quiet; | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet | RemoveAndSuggest(response, tileId) => // Only side effects in the editor are performed here add_suggestion(~response, tileId); model |> Updated.return_quiet; - | SwitchMode(mode) => - {...model, show_history: false} |> Updated.return_quiet | Describe(content, mode, chat_id) => + let (past_chats, _) = get_mode_info(mode, model); let updated_chats = - switch (mode) { - | SimpleChat => - // Only update the descriptor of the specific chat with matching ID - { - ...model.chats, - curr_simple_chat: - model.chats.curr_simple_chat.id == chat_id - ? {...model.chats.curr_simple_chat, descriptor: content} - : model.chats.curr_simple_chat, - past_simple_chats: - List.map( - (c: Model.chat) => - c.id == chat_id ? {...c, descriptor: content} : c, - model.chats.past_simple_chats, - ), - } - | CodeSuggestion => { - ...model.chats, - curr_suggestion_chat: - model.chats.curr_suggestion_chat.id == chat_id - ? {...model.chats.curr_suggestion_chat, descriptor: content} - : model.chats.curr_suggestion_chat, - past_suggestion_chats: - List.map( - (c: Model.chat) => - c.id == chat_id ? {...c, descriptor: content} : c, - model.chats.past_suggestion_chats, - ), - } - | TaskCompletion => { - ...model.chats, - curr_completion_chat: - model.chats.curr_completion_chat.id == chat_id - ? {...model.chats.curr_completion_chat, descriptor: content} - : model.chats.curr_completion_chat, - past_completion_chats: - List.map( - (c: Model.chat) => - c.id == chat_id ? {...c, descriptor: content} : c, - model.chats.past_completion_chats, - ), - } - }; - - {...model, chats: updated_chats} |> Updated.return_quiet; + Id.Map.update( + chat_id, + opt_chat => + switch (opt_chat) { + | Some(chat: Model.chat) => Some({...chat, descriptor: content}) + | None => None + }, + past_chats, + ); + resculpt_model(mode, model, updated_chats, chat_id) + |> Updated.return_quiet; | SwitchChat(chat_id) => let mode = settings.assistant.mode; - let find_by_id = - (chats: Model.chats, id: Id.t, ~get_id: Model.chat => Id.t) => { - switch (mode) { - | SimpleChat => - List.find_opt(item => get_id(item) == id, chats.past_simple_chats) - | CodeSuggestion => - List.find_opt( - item => get_id(item) == id, - chats.past_suggestion_chats, - ) - | TaskCompletion => - List.find_opt( - item => get_id(item) == id, - chats.past_completion_chats, - ) - }; - }; - - // Store current chat back into past_chats list - let updated_past_chats = - switch (mode) { - | SimpleChat => { - ...model.chats, - past_simple_chats: - List.map( - (chat: Model.chat) => - chat.id == model.chats.curr_simple_chat.id - ? model.chats.curr_simple_chat : chat, - model.chats.past_simple_chats, - ), - } - | CodeSuggestion => { - ...model.chats, - past_suggestion_chats: - List.map( - (chat: Model.chat) => - chat.id == model.chats.curr_suggestion_chat.id - ? model.chats.curr_suggestion_chat : chat, - model.chats.past_suggestion_chats, - ), - } - | TaskCompletion => { - ...model.chats, - past_completion_chats: - List.map( - (chat: Model.chat) => - chat.id == model.chats.curr_completion_chat.id - ? model.chats.curr_completion_chat : chat, - model.chats.past_completion_chats, - ), - } - }; - - // Get the chat we're switching to - let curr_chat = - Option.get( - find_by_id(updated_past_chats, chat_id, ~get_id=chat => chat.id), - ); - - // Now update the current chat - let final_chats = - switch (mode) { - | SimpleChat => {...updated_past_chats, curr_simple_chat: curr_chat} - | CodeSuggestion => { - ...updated_past_chats, - curr_suggestion_chat: curr_chat, - } - | TaskCompletion => { - ...updated_past_chats, - curr_completion_chat: curr_chat, - } - }; - - {...model, chats: final_chats} |> Updated.return_quiet; + let (past_chats, _) = get_mode_info(mode, model); + resculpt_model(mode, model, past_chats, chat_id) |> Updated.return_quiet; }; }; }; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index 4f40dfcd8b..0dd3398500 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -89,8 +89,10 @@ let end_chat_button = (~globals: Globals.t): Node.t => { Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( - ~attrs=[clss(["chat-button"])], - [Widgets.button_named(~tooltip, None, end_chat)], + ~attrs=[clss(["chat-button"]), Attr.on_click(end_chat)], + [ + Widgets.button_named(~tooltip, None, _ => Virtual_dom.Vdom.Effect.Ignore), + ], ); }; @@ -102,18 +104,27 @@ let new_chat_button = (~globals: Globals.t, ~inject): Node.t => { Virtual_dom.Vdom.Effect.Stop_propagation, ]); div( - ~attrs=[clss(["add-button"])], - [Widgets.button(~tooltip, Icons.add, new_chat)], + ~attrs=[clss(["add-button"]), Attr.on_click(new_chat)], + [ + Widgets.button(~tooltip, Icons.add, _ => Virtual_dom.Vdom.Effect.Ignore), + ], ); }; let history_button = (~globals: Globals.t, ~inject): Node.t => { let tooltip = "Past Chats"; let history = _ => - Virtual_dom.Vdom.Effect.Many([inject(Assistant.Update.History)]); + Virtual_dom.Vdom.Effect.Many([ + inject(Assistant.Update.History), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); div( - ~attrs=[clss(["history-button"])], - [Widgets.button(~tooltip, Icons.history, history)], + ~attrs=[clss(["history-button"]), Attr.on_click(history)], + [ + Widgets.button(~tooltip, Icons.history, _ => + Virtual_dom.Vdom.Effect.Ignore + ), + ], ); }; @@ -237,18 +248,8 @@ let api_input = }; let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch ( - key, - ListUtil.last_opt( - switch (settings.mode) { - | SimpleChat => assistantModel.chats.curr_simple_chat.messages - | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages - | TaskCompletion => assistantModel.chats.curr_completion_chat.messages - }, - ), - ) { - | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore - | (Some("Enter"), _) => submit_key() + switch (key) { + | Some("Enter") => submit_key() | _ => Virtual_dom.Vdom.Effect.Ignore }; }; @@ -307,11 +308,13 @@ let message_input = }; JsUtil.log("Message sent: " ++ message.content); Virtual_dom.Vdom.Effect.Many([ - inject(Assistant.Update.SendMessage(message)), + inject(Assistant.Update.SendTextMessage(message)), Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; - + let (past_chats, curr_chat) = + Assistant.Update.get_mode_info(settings.mode, assistantModel); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; let send_message = _ => { let message = Js.Opt.case( @@ -331,16 +334,7 @@ let message_input = }; let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch ( - key, - ListUtil.last_opt( - switch (settings.mode) { - | SimpleChat => assistantModel.chats.curr_simple_chat.messages - | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages - | TaskCompletion => assistantModel.chats.curr_completion_chat.messages - }, - ), - ) { + switch (key, ListUtil.last_opt(curr_messages)) { | (_, Some({party: LLM, code: None, content: "...", collapsed: false})) => Virtual_dom.Vdom.Effect.Ignore | (Some("Enter"), _) => send_message() | _ => Virtual_dom.Vdom.Effect.Ignore @@ -369,17 +363,7 @@ let message_input = ], (), ), - switch ( - ListUtil.last_opt( - switch (settings.mode) { - | SimpleChat => assistantModel.chats.curr_simple_chat.messages - | CodeSuggestion => - assistantModel.chats.curr_suggestion_chat.messages - | TaskCompletion => - assistantModel.chats.curr_completion_chat.messages - }, - ) - ) { + switch (ListUtil.last_opt(curr_messages)) { | Some({party: LLM, code: None, content: "...", collapsed: false}) => div( ~attrs=[ @@ -402,6 +386,7 @@ let message_input = ); }; +// For aesthetic purposes only :) let loading_dots = () => { div( ~attrs=[clss(["loading-dots"])], @@ -428,7 +413,9 @@ let message_display = Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; - + let (past_chats, curr_chat) = + Assistant.Update.get_mode_info(settings.mode, assistantModel); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; let message_nodes = List.flatten( List.mapi( @@ -442,19 +429,35 @@ let message_display = ~attrs=[ clss([ "message-container", - message.party == LLM ? "llm" : "ls", + switch (message.party) { + | LS => "ls" + | LLM => "llm" + | System => "system" + }, ]), Attr.on_click(_ => toggle_collapse(index)), ], [ div( ~attrs=[clss(["message-identifier"])], - [text(message.party == LLM ? "LLM" : "LS")], + [ + text( + switch (message.party) { + | LS => "User" + | LLM => "Assistant" + | System => "System" + }, + ), + ], ), div( ~attrs=[ clss([ - message.party == LLM ? "llm-message" : "ls-message", + switch (message.party) { + | LS => "ls-message" + | LLM => "llm-message" + | System => "system-message" + }, ]), ], [ @@ -525,19 +528,35 @@ let message_display = ~attrs=[ clss([ "message-container", - message.party == LLM ? "llm" : "ls", + switch (message.party) { + | LS => "ls" + | LLM => "llm" + | System => "system" + }, ]), Attr.on_click(_ => toggle_collapse(index)), ], [ div( ~attrs=[clss(["message-identifier"])], - [text(message.party == LLM ? "Assistant" : "User")], + [ + text( + switch (message.party) { + | LS => "User" + | LLM => "Assistant" + | System => "System" + }, + ), + ], ), div( ~attrs=[ clss([ - message.party == LLM ? "llm-message" : "ls-message", + switch (message.party) { + | LS => "ls-message" + | LLM => "llm-message" + | System => "system-message" + }, ]), ], [ @@ -564,11 +583,7 @@ let message_display = ] } }, - switch (settings.mode) { - | SimpleChat => assistantModel.chats.curr_simple_chat.messages - | CodeSuggestion => assistantModel.chats.curr_suggestion_chat.messages - | TaskCompletion => assistantModel.chats.curr_completion_chat.messages - }, + curr_messages, ), ); div(~attrs=[clss(["message-display-container"])], message_nodes); @@ -610,13 +625,9 @@ let history_menu = ~inject, ) : Node.t => { - let past_chats = - switch (settings.mode) { - | SimpleChat => assistantModel.chats.past_simple_chats - | CodeSuggestion => assistantModel.chats.past_suggestion_chats - | TaskCompletion => assistantModel.chats.past_completion_chats - }; - + let (past_chats, curr_chat) = + Assistant.Update.get_mode_info(settings.mode, assistantModel); + let chrono_past_chats = Assistant.Model.sorted_chats(past_chats); div( ~attrs=[clss(["history-menu"])], [ @@ -627,9 +638,7 @@ let history_menu = (chat: Assistant.Model.chat) => div( ~attrs=[ - chat.id == assistantModel.chats.curr_simple_chat.id - || chat.id == assistantModel.chats.curr_suggestion_chat.id - || chat.id == assistantModel.chats.curr_completion_chat.id + chat.id == curr_chat.id ? clss(["history-menu-item", "active"]) : clss(["history-menu-item"]), Attr.on_click(e => { @@ -668,22 +677,26 @@ let history_menu = ], ), div( - ~attrs=[clss(["delete-chat-button"])], - [ - Widgets.button(~tooltip="Delete chat", Icons.trash, _ => + ~attrs=[ + clss(["delete-chat-button"]), + Attr.on_click(_ => Virtual_dom.Vdom.Effect.Many([ inject(Assistant.Update.DeleteChat(chat.id)), Virtual_dom.Vdom.Effect.Stop_propagation, - Virtual_dom.Vdom.Effect.Prevent_default, ]) ), ], + [ + Widgets.button(~tooltip="Delete chat", Icons.trash, _ => + Virtual_dom.Vdom.Effect.Ignore + ), + ], ), ], ), ], ), - past_chats, + chrono_past_chats, ), ), ], diff --git a/src/haz3lweb/util/API/OpenRouter.re b/src/haz3lweb/util/API/OpenRouter.re index 7e61d0a2df..d06cfb8515 100644 --- a/src/haz3lweb/util/API/OpenRouter.re +++ b/src/haz3lweb/util/API/OpenRouter.re @@ -90,22 +90,19 @@ let lookup_key = (llm: chat_models) => | DeepSeek_V3 => Store.Generic.load("API") }; -let chat = (~key, ~body, ~handler): unit => - switch (key) { - | None => print_endline("API: OpenAI KEY NOT FOUND") - | Some(api_key) => - print_endline("API: POSTing OpenRouter request"); - request( - ~method=POST, - ~url="https://openrouter.ai/api/v1/chat/completions", - ~headers=[ - ("Content-Type", "application/json"), - ("Authorization", "Bearer " ++ api_key), - ], - ~body, - handler, - ); - }; +let chat = (~key, ~body, ~handler): unit => { + print_endline("API: POSTing OpenRouter request"); + request( + ~method=POST, + ~url="https://openrouter.ai/api/v1/chat/completions", + ~headers=[ + ("Content-Type", "application/json"), + ("Authorization", "Bearer " ++ key), + ], + ~body, + handler, + ); +}; let start_chat = (~params, ~key, prompt: prompt, handler): unit => { let body = body(~params, prompt); diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 427e67a68e..3a3e5d68be 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -2,7 +2,7 @@ display: flex; flex: 1; width: 25em; - padding: 1em; + padding: 8px; flex-direction: column; overflow-y: auto; gap: 1em; @@ -163,6 +163,10 @@ align-self: flex-start; } +#assistant .system .message-identifier { + align-self: flex-start; +} + #assistant .ls .message-identifier { align-self: flex-end; } @@ -171,6 +175,10 @@ align-items: flex-start; } +#assistant .system { + align-items: flex-start; +} + #assistant .ls { align-items: flex-end; } @@ -201,6 +209,15 @@ word-wrap: break-word; } +#assistant .system-message { + padding: 8px 12px; + background-color: var(--ERRHOLE); + border-radius: 12px; + font-size: 1em; + max-width: 80%; + word-wrap: break-word; +} + #assistant .ls-message { padding: 8px 12px; background-color: var(--T3); From 1ddcbdd315ea2123294c3923ac34defa56948d49 Mon Sep 17 00:00:00 2001 From: russell-rozenbaum Date: Mon, 3 Mar 2025 12:19:59 -0500 Subject: [PATCH 46/50] adds resuggest button with limited functionality - code is suggested at cursor position, so a todo will be to place code at prior suggestion location... I am imagining this may more involved than it seems, eg. what happens when code is signicantly restructured? My inclination is to remember the hole id, and simply resuggest the code in that hole if it still exists (with same id) --- .../app/helpful-assistant/Assistant.re | 41 ++++++++----------- .../app/helpful-assistant/AssistantView.re | 22 +++++++++- src/haz3lweb/view/Page.re | 14 ++++++- src/haz3lweb/www/style/assistant.css | 16 +++++++- src/haz3lweb/www/style/variables.css | 1 + 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index 951424c1cf..5732e07cf2 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -16,7 +16,7 @@ module Model = { [@deriving (show({with_path: false}), sexp, yojson)] type message = { party, - code: option(Segment.t), + code: option((Segment.t, option(Id.t))), content: string, collapsed: bool, }; @@ -128,11 +128,13 @@ module Update = { | ToggleCollapse(int) | SelectLLM(OpenRouter.chat_models) | RemoveAndSuggest(string, Id.t) + | Resuggest(string, Id.t) | Describe(string, AssistantSettings.mode, Id.t) | SwitchChat(Id.t); let code_message_of_str = - (response: string, party: Model.party): Model.message => { + (response: string, party: Model.party, tileId: option(Id.t)) + : Model.message => { /* Alternate method using Detruct and Insert. We need a memory of cursor location for this however. let z = editor.editor.state.zipper; let z = Option.get(Destruct.go(Direction.Left, z)); @@ -158,7 +160,7 @@ module Update = { Zipper.smart_seg(~dump_backpack=true, ~erase_buffer=true, z); { party, - code: Some(segment_of_response), + code: Some((segment_of_response, tileId)), content: response, collapsed: String.length(response) >= 200, }; @@ -181,20 +183,6 @@ module Update = { }; }; - let react = - ( - ~response: string, - ~code_suggestion: bool, - ~mode: AssistantSettings.mode, - ~chat_id: Id.t, - ) - : t => { - // let response = response |> sanitize_response |> quote; - code_suggestion - ? Respond(code_message_of_str(response, LLM), mode, chat_id) - : Respond(text_message_of_str(response, LLM), mode, chat_id); - }; - let await_llm_response: Model.message = { party: LLM, code: None, @@ -522,11 +510,10 @@ module Update = { switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => schedule_action( - react( - ~response=content, - ~code_suggestion=false, - ~mode, - ~chat_id=curr_chat.id, + Respond( + text_message_of_str(content, LLM), + mode, + curr_chat.id, ), ) | None => print_endline("Assistant: response parse failed") @@ -615,7 +602,7 @@ module Update = { let prompt = ListUtil.concat_strings(messages); let message: Model.message = { party: LS, - code: Some(sketch_seg), + code: Some((sketch_seg, None)), content: prompt, collapsed: String.length(prompt) >= 200, }; @@ -679,7 +666,7 @@ module Update = { }; }; | ErrorRespond(response, ci, fuel, tileId, mode, chat_id) => - let message = code_message_of_str(response, LLM); + let message = code_message_of_str(response, LLM, Some(tileId)); switch (ChatLSP.Prompt.mk_error(ci, response)) { | None => // No error, all good. Concat and return suggestion. @@ -811,7 +798,11 @@ module Update = { | SelectLLM(llm) => {...model, llm} |> Updated.return_quiet | RemoveAndSuggest(response, tileId) => // Only side effects in the editor are performed here - add_suggestion(~response, tileId); + add_suggestion(~response, tileId, false); + model |> Updated.return_quiet; + | Resuggest(response, tileId) => + // Only side effects in the editor are performed here + add_suggestion(~response, tileId, true); model |> Updated.return_quiet; | Describe(content, mode, chat_id) => let (past_chats, _) = get_mode_info(mode, model); diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index 0dd3398500..b235fbc719 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -421,7 +421,7 @@ let message_display = List.mapi( (index: int, message: Assistant.Model.message) => { switch (message.code) { - | Some(sketch) => + | Some((sketch, tileId)) => message.content == "..." && message.party == LLM ? [loading_dots()] : [ @@ -479,6 +479,26 @@ let message_display = : text(message.content), ], ), + message.party == LLM && tileId != None + ? div( + ~attrs=[ + clss(["resuggest-button"]), + Attr.on_click(_ => + Virtual_dom.Vdom.Effect.Many([ + inject( + Assistant.Update.Resuggest( + message.content, + Option.get(tileId), + ), + ), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + ), + Attr.title("Resuggest"), + ], + [text("resuggest")], + ) + : None, ], ), div( diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 74ea8cc990..f5cd2c9b75 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -268,8 +268,18 @@ module Update = { | Exercises(m) => List.nth(m.exercises, m.current).cells.user_impl // Todo this is an error }; open Haz3lcore; - let add_suggestion = (~response: string, tile: Id.t) => { - // Create a sequence of actions to handle the suggestion + let add_suggestion = (~response: string, tile: Id.t, resuggest: bool) => { + /* + let tile_content = + TileMap.find_opt(tile, ed.editor.editor.syntax.tiles) + |> Option.map((tile: Tile.t) => tile.label) + |> Option.map(List.hd); + switch (tile_content) { + | Some(content) => print_endline("tile_content: " ++ content) + | None => print_endline("tile_content: None") + }; + */ + print_endline("resuggest: " ++ string_of_bool(resuggest)); let actions = [ Action.Select(Tile(Id(tile, Direction.Left))), Action.Destruct(Direction.Left), diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 3a3e5d68be..2d70d87b33 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -149,6 +149,7 @@ display: flex; flex-direction: column; margin-bottom: 10px; + position: relative; } #assistant .message-identifier { @@ -521,7 +522,7 @@ } #assistant .history-menu-item:hover:not(:has(.button:hover)) { - background-color: var(--T3); + background-color: var(--T4); padding-left: 18px; } @@ -581,4 +582,17 @@ #assistant .history-menu-item:last-child { border-bottom: none; +} + +#assistant .resuggest-button { + color: var(--STONE); + padding: 4px 8px; + cursor: pointer; + opacity: 0.6; + transition: all 0.2s ease; + font-size: 0.6em; +} + +#assistant .resuggest-button:hover { + opacity: 0.9 !important; } \ No newline at end of file diff --git a/src/haz3lweb/www/style/variables.css b/src/haz3lweb/www/style/variables.css index 143829787d..ab606238f3 100644 --- a/src/haz3lweb/www/style/variables.css +++ b/src/haz3lweb/www/style/variables.css @@ -24,6 +24,7 @@ --T1: oklch(97% 0.025 90); /* buffer shards */ --T2: oklch(from var(--T1) 94% c h); /* projector shards */ --T3: oklch(from var(--T1) 91% c h); /* result background */ + --T4: oklch(from var(--T1) 88% c h); /* darker background, used in assistant */ /* MOLTEN - under construction */ --Y0: oklch(95% 0.05 90); /* menu fill */ From 11934dcdfeff129820534dea83a1264762d9991a Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Wed, 5 Mar 2025 02:11:38 -0500 Subject: [PATCH 47/50] added CoT reasoning for completions --- .../app/helpful-assistant/Assistant.re | 110 +++++++++++++++--- src/haz3lweb/app/helpful-assistant/ChatLSP.re | 94 +++++++++++++-- 2 files changed, 177 insertions(+), 27 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index 5732e07cf2..f3ebb14f40 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -244,14 +244,17 @@ module Update = { model: Model.t, message: Model.message, chat_id: Id.t, + ~is_final: bool, ) => { let (past_chats, _) = get_mode_info(mode, model); let chat_to_update = Id.Map.find(chat_id, past_chats); let messages = { switch (message.party) { | LS => chat_to_update.messages @ [message, await_llm_response] - | LLM => ListUtil.leading(chat_to_update.messages) @ [message] - | System => ListUtil.leading(chat_to_update.messages) @ [message] + | LLM => + let messages = ListUtil.leading(chat_to_update.messages) @ [message]; + is_final ? messages : messages @ [await_llm_response]; + | System => chat_to_update.messages @ [message] }; }; Model.{ @@ -499,6 +502,7 @@ module Update = { collapsed: false, }, curr_chat.id, + ~is_final=true, ) |> Updated.return_quiet | Some(prompt) => @@ -519,7 +523,13 @@ module Update = { | None => print_endline("Assistant: response parse failed") } ); - add_message_to_model(mode, model, message, curr_chat.id) + add_message_to_model( + mode, + model, + message, + curr_chat.id, + ~is_final=true, + ) |> Updated.return_quiet; | None => add_message_to_model( @@ -532,6 +542,7 @@ module Update = { collapsed: false, }, curr_chat.id, + ~is_final=true, ) |> Updated.return_quiet }; @@ -572,7 +583,7 @@ module Update = { {...model, show_history: !model.show_history} |> Updated.return_quiet | Respond(message, mode, chat_id) => check_descriptor(~model, ~schedule_action, ~message, ~mode, ~chat_id); - add_message_to_model(mode, model, message, chat_id) + add_message_to_model(mode, model, message, chat_id, ~is_final=true) |> Updated.return_quiet; | SendSketchMessage(tileId, mode) => // Capture the chat we're updating here. This will propogate. @@ -606,6 +617,7 @@ module Update = { content: prompt, collapsed: String.length(prompt) >= 200, }; + print_endline(prompt); /* Old code. Don't need to collect chat here, leads to far too long of prompts. let collected_chat = switch (mode) { @@ -648,7 +660,13 @@ module Update = { | None => print_endline("Assistant: response parse failed") } ); - add_message_to_model(mode, model, message, curr_chat.id) + add_message_to_model( + mode, + model, + message, + curr_chat.id, + ~is_final=true, + ) |> Updated.return_quiet; | None => add_message_to_model( @@ -661,28 +679,70 @@ module Update = { collapsed: false, }, curr_chat.id, + ~is_final=true, ) |> Updated.return_quiet }; }; | ErrorRespond(response, ci, fuel, tileId, mode, chat_id) => - let message = code_message_of_str(response, LLM, Some(tileId)); - switch (ChatLSP.Prompt.mk_error(ci, response)) { + // Split response into discussion and completion + let code_pattern = + Str.regexp( + "\\(\\(.\\|\n\\)*\\)```[ \n]*\\([^`]+\\)[ \n]*```\\(\\(.\\|\n\\)*\\)", + ); + let (discussion, completion) = + if (Str.string_match(code_pattern, response, 0)) { + let before = String.trim(Str.matched_group(1, response)); + let code = String.trim(Str.matched_group(3, response)); + (before, code); + } else { + print_endline("Regex match failed for: " ++ response); + ("", response); // Fallback if no code block found + }; + print_endline("Response: " ++ response); + print_endline("Discussion: " ++ discussion); + print_endline("Completion: " ++ completion); + // First add the discussion message + let discussion_message = text_message_of_str(discussion, LLM); + let model_with_discussion = + add_message_to_model( + mode, + model, + discussion_message, + chat_id, + ~is_final=false, + ); + + // Then handle the completion as before + let completion_message = + code_message_of_str(completion, LLM, Some(tileId)); + switch (ChatLSP.Prompt.mk_error(ci, completion)) { | None => - // No error, all good. Concat and return suggestion. - print_endline("ERROR ROUNDS (Non-error Response): " ++ response); - check_descriptor(~model, ~schedule_action, ~message, ~mode, ~chat_id); - schedule_action(RemoveAndSuggest(response, tileId)); + print_endline("ERROR ROUNDS (Non-error Response): " ++ completion); + check_descriptor( + ~model, + ~schedule_action, + ~message=completion_message, + ~mode, + ~chat_id, + ); + schedule_action(RemoveAndSuggest(completion, tileId)); | Some(error) => - // There is some error, so perform an error round print_endline("ERROR ROUNDS (Error): " ++ error); - print_endline("ERROR ROUNDS (Error-causing Response): " ++ response); + print_endline( + "ERROR ROUNDS (Error-causing Response): " ++ completion, + ); schedule_action( SendErrorMessage(error, ci, fuel - 1, tileId, mode, chat_id), ); }; - // Remove await_llm_response (... animation) and concat LLM's suggestion - add_message_to_model(mode, model, message, chat_id) + add_message_to_model( + mode, + model_with_discussion, + completion_message, + chat_id, + ~is_final=true, + ) |> Updated.return_quiet; | SendErrorMessage(error, ci, fuel, tileId, mode, chat_id) => let error_message = @@ -693,7 +753,14 @@ module Update = { ); // check that fuel is not 0 if (fuel < 0) { - let model = add_message_to_model(mode, model, error_message, chat_id); + let model = + add_message_to_model( + mode, + model, + error_message, + chat_id, + ~is_final=true, + ); add_message_to_model( mode, model, @@ -707,6 +774,7 @@ module Update = { collapsed: false, }, chat_id, + ~is_final=true, ) |> Updated.return_quiet; } else { @@ -728,6 +796,7 @@ module Update = { collapsed: false, }, chat_id, + ~is_final=true, ) |> Updated.return_quiet | Some(openrouter_prompt) => @@ -748,7 +817,13 @@ module Update = { | None => print_endline("Assistant: response parse failed") } ); - add_message_to_model(mode, model, error_message, chat_id) + add_message_to_model( + mode, + model, + error_message, + chat_id, + ~is_final=true, + ) |> Updated.return_quiet; | None => add_message_to_model( @@ -761,6 +836,7 @@ module Update = { collapsed: false, }, chat_id, + ~is_final=true, ) |> Updated.return_quiet }; diff --git a/src/haz3lweb/app/helpful-assistant/ChatLSP.re b/src/haz3lweb/app/helpful-assistant/ChatLSP.re index 790c9cb738..aa05fd605c 100644 --- a/src/haz3lweb/app/helpful-assistant/ChatLSP.re +++ b/src/haz3lweb/app/helpful-assistant/ChatLSP.re @@ -587,9 +587,14 @@ let List.length: [(String, Bool)]-> Int = |}, RelevantType.expected(Ana(Typ.fresh(Int)), ~ctx=[]), {| +DISCUSSION +The function List.length takes a list of (String, Bool) tuples and returns an Int. The natural way to compute the length of a list is through recursion. +The base case for an empty list is 0, and for a non-empty list, we increment the count and recursively call List.length on the tail. +``` case xs | [] => 0 -| _::xs => 1 + List.length(xs)|}, +| _::xs => 1 + List.length(xs) +```|}, ), ( {| @@ -604,10 +609,14 @@ let List.mapi: ((Int, Bool) -> Bool, [Bool]) -> [Bool]= ~ctx=[], ), {| +DISCUSSION +The function List.mapi applies a function f to each element of a list while keeping track of the index. The helper function go does this recursively. +The base case returns an empty list. In the recursive case, f(idx, hd) is applied to the head, and go(idx + 1, tl) is called recursively on the tail to process the rest of the list. +``` case xs | [] => [] | hd::tl => f(idx, hd)::go(idx + 1, tl) -|}, +```|}, ), ( {| @@ -625,17 +634,30 @@ in ~ctx=[], ), {| +DISCUSSION +The function total_capacity takes a Container and returns an Int. The Pod variant stores a Bool, which likely indicates whether the pod is active. +The condition if !b && true simplifies to if !b, meaning inactive pods have a capacity of 1, while active ones have 0. +The CapsuleCluster variant contains two integers, which are multiplied together to represent the total capacity. +``` fun c -> case c | Pod(b) => if !b && true then 1 else 0 | CapsuleCluster(x, y) => x * y end +``` |}, ), ( "let f = ?? in f(5)", RelevantType.expected(Syn, ~ctx=[]), - "fun x:Int -> ??", + {| +DISCUSSION +The expression let f = ?? in f(5) means f should be a function that can take an integer input. A function of type fun x:Int -> ?? is defined, but its body is missing. +Since no constraints are placed on the output type, the hole could be filled with any valid expression. +``` +fun x:Int -> ?? +``` + |}, ), ( {|let triple = (4, 8, true) in @@ -646,12 +668,27 @@ fun maybe_num -> | Some(x) => ?? | None => if !condition then 0 else y + 1 end in|}, RelevantType.expected(Ana(Typ.fresh(Int)), ~ctx=[]), - "x", + {| +DISCUSSION +The function get extracts a value from an Option type. If Some(x), the function should return x, as x is already of type Int. +The None case considers a condition; if !condition is true, it returns 0, otherwise, it returns y + 1. +Since x is an Int, returning it in the Some case maintains type consistency. +``` +x +``` + |}, ), ( "let num_or_zero = fun maybe_num ->\n case maybe_num\n | Some(num) => ?? \n| None => 0 end in", RelevantType.expected(Syn, ~ctx=[]), - "num", + {| +DISCUSSION +The function num_or_zero takes an Option(Int) and returns an Int. If the input is Some(num), it should return num, as num is already an integer. +If None, the function defaults to returning 0. This ensures type consistency while preserving the stored number when available. +``` +num +``` + |}, ), ( "let merge_sort: [Int]->[Int] =\n??\nin\nmerge_sort([4,1,3,7,2])", @@ -666,12 +703,29 @@ fun maybe_num -> ), ~ctx=[], ), - "fun list ->\nlet split: [Int]->([Int],[Int]) = fun left, right -> ?\nin\nlet merge: ([Int],[Int])->[Int]= ?\nin\nlet merge_sort_helper: [Int]->[Int]= ?\nin\nmerge_sort_helper(list)", + {| +DISCUSSION +The function merge_sort sorts a list of integers. A common approach to implementing merge sort involves: +1. Splitting the list into two halves (split). +2. Recursively sorting both halves (merge_sort_helper). +3. Merging the sorted halves (merge). +The provided structure follows this approach, so we use helper functions to complete the sorting logic. +``` +fun list ->\nlet split: [Int]->([Int],[Int]) = fun left, right -> ?\nin\nlet merge: ([Int],[Int])->[Int]= ?\nin\nlet merge_sort_helper: [Int]->[Int]= ?\nin\nmerge_sort_helper(list) +``` + |}, ), ( "type MenuItem =\n+ Breakfast(Int, Int)\n+ Lunch(Float)\nin\nlet per_lunch_unit = 0.95 in\nlet price: MenuItem-> Float = fun m ->\ncase m\n| Breakfast(x, y) => ??\n| Lunch(f) => f *. per_lunch_unit\nend\nin price(Breakfast(1,2))/.3.", RelevantType.expected(Ana(Typ.fresh(Var("MenuItem"))), ~ctx=[]), - "fun m ->\ncase m\n| Breakfast(x, y) => ??\n| Lunch(f) => f *. per_lunch_unit\nend", + {| +DISCUSSION +The function price computes the cost of a MenuItem. The Lunch variant already has a predefined price calculation. For Breakfast(x, y), an expression must return a Float, but the completion is missing. +The function should ensure a proper numeric computation based on x and y. +``` +fun m ->\ncase m\n| Breakfast(x, y) => ??\n| Lunch(f) => f *. per_lunch_unit\nend +``` + |}, ), ( {| @@ -703,9 +757,14 @@ test 2 == List.nth(List.sort(fun a, b -> a if n == 0 then 1.0 else 2.0", + " ```", + "- The code completion should be a functional, idiomatic replacement for the program hole marked '??' in the provided program sketch", // Uncomment either of the following lines to test error rounds // "- However, make sure that your initial response CAUSES A TYPE ERROR in the program. Then, fix it in your second response", // "- However, it is CRITICALLY important to make sure that your response ALWAYS CAUSES A TYPE ERROR in the program, no matter how many times you are re-prompted", - "- Reply ONLY with a SINGLE replacement term for the unqiue distinguished hole marked '??'", - "- Reply ONLY with code", + "- Reply ONLY with a SINGLE replacement term for the unique distinguished hole marked '??'", "- DO NOT suggest more replacements for other holes in the sketch (marked '?'), or implicit holes", "- This is critical, and I am going to reiterate it: DO NOT suggest more than one replacement term. It should ONLY be for the hole marked '??'", "- For example, if you are being asked to complete 'let f = ? in ??', your response should ONLY be a single replacement term for the hole marked '??', NOT a replacement term for the hole marked '?'", From 15f6a52acedc6b2bfde81ff5f8c4d6a455ca5223 Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Thu, 6 Mar 2025 00:48:52 -0500 Subject: [PATCH 48/50] adds support for showing/hiding API key --- .../app/helpful-assistant/Assistant.re | 9 ++++-- .../app/helpful-assistant/AssistantView.re | 32 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index f3ebb14f40..aa35d2f4aa 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -51,7 +51,8 @@ module Model = { current_chats, chat_history, llm: OpenRouter.chat_models, - show_history: bool // TODO: Move this to AssistantSettings.re + show_history: bool, // TODO: Move this to AssistantSettings.re + show_api_key: bool, }; let init_simple_chat = { @@ -103,6 +104,7 @@ module Model = { }, llm: Gemini_Flash_Lite_2_0, show_history: false, + show_api_key: false, }; }; @@ -130,7 +132,8 @@ module Update = { | RemoveAndSuggest(string, Id.t) | Resuggest(string, Id.t) | Describe(string, AssistantSettings.mode, Id.t) - | SwitchChat(Id.t); + | SwitchChat(Id.t) + | ToggleAPIVisibility; let code_message_of_str = (response: string, party: Model.party, tileId: option(Id.t)) @@ -898,6 +901,8 @@ module Update = { let mode = settings.assistant.mode; let (past_chats, _) = get_mode_info(mode, model); resculpt_model(mode, model, past_chats, chat_id) |> Updated.return_quiet; + | ToggleAPIVisibility => + {...model, show_api_key: !model.show_api_key} |> Updated.return_quiet }; }; }; diff --git a/src/haz3lweb/app/helpful-assistant/AssistantView.re b/src/haz3lweb/app/helpful-assistant/AssistantView.re index b235fbc719..e97b867654 100644 --- a/src/haz3lweb/app/helpful-assistant/AssistantView.re +++ b/src/haz3lweb/app/helpful-assistant/AssistantView.re @@ -229,6 +229,13 @@ let api_input = Virtual_dom.Vdom.Effect.Stop_propagation, ]); }; + + let toggle_visibility = _ => + Virtual_dom.Vdom.Effect.Many([ + inject(Assistant.Update.ToggleAPIVisibility), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + let submit_key = _ => { let message = Js.Opt.case( @@ -246,6 +253,7 @@ let api_input = ); handle_submission(message); }; + let handle_keydown = event => { let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); switch (key) { @@ -261,7 +269,7 @@ let api_input = ~attrs=[ Attr.id("api-input"), Attr.placeholder("Enter your API key..."), - Attr.type_("text"), + Attr.type_("password"), Attr.property("autocomplete", Js.Unsafe.inject("off")), Attr.on_focus(_ => signal(MakeActive(ScratchMode.Selection.TextBox)) @@ -275,15 +283,27 @@ let api_input = ~attrs=[clss(["chat-button"]), Attr.on_click(submit_key)], [Widgets.button_named(~tooltip="Update API Key", None, submit_key)], ), + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(toggle_visibility)], + [ + Widgets.button_named( + ~tooltip="Show/Hide Key", + None, + toggle_visibility, + ), + ], + ), div(~attrs=[clss(["text-display"])], [text("Current API Key:\n")]), div( - ~attrs=[clss(["api-key-display"])], + ~attrs=[clss(["api-key-display"]), Attr.id("api-key-display")], [ text( - Option.value( - Store.Generic.load("API"), - ~default="No API key set", - ), + switch (Store.Generic.load("API")) { + | Some(key) when String.length(key) > 0 => + assistantModel.show_api_key + ? key : String.make(String.length(key), '*') + | _ => "No API key set" + }, ), ], ), From a3cc022d891eafef7dd5c2a5a044175746abc421 Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Thu, 6 Mar 2025 02:21:32 -0500 Subject: [PATCH 49/50] added horizontal scroll to sketches in sidebar --- .../app/helpful-assistant/Assistant.re | 19 +++++-- src/haz3lweb/www/style/assistant.css | 56 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/haz3lweb/app/helpful-assistant/Assistant.re b/src/haz3lweb/app/helpful-assistant/Assistant.re index aa35d2f4aa..36288d4808 100644 --- a/src/haz3lweb/app/helpful-assistant/Assistant.re +++ b/src/haz3lweb/app/helpful-assistant/Assistant.re @@ -376,7 +376,8 @@ module Update = { switch (OpenRouter.handle_chat(req)) { | Some({content, _}) => schedule_action(Describe(content, mode, chat.id)) - | None => print_endline("Assistant: response parse failed") + | None => + print_endline("Assistant: response parse failed (form_descriptor)") } ); }; @@ -523,7 +524,10 @@ module Update = { curr_chat.id, ), ) - | None => print_endline("Assistant: response parse failed") + | None => + print_endline( + "Assistant: response parse failed (SendTextMessage)", + ) } ); add_message_to_model( @@ -620,7 +624,6 @@ module Update = { content: prompt, collapsed: String.length(prompt) >= 200, }; - print_endline(prompt); /* Old code. Don't need to collect chat here, leads to far too long of prompts. let collected_chat = switch (mode) { @@ -660,7 +663,10 @@ module Update = { curr_chat.id, ), ); - | None => print_endline("Assistant: response parse failed") + | None => + print_endline( + "Assistant: response parse failed (SendSketchMessage)", + ) } ); add_message_to_model( @@ -817,7 +823,10 @@ module Update = { schedule_action( ErrorRespond(content, ci, fuel, tileId, mode, curr_chat.id), ) - | None => print_endline("Assistant: response parse failed") + | None => + print_endline( + "Assistant: response parse failed (SendErrorMessage)", + ) } ); add_message_to_model( diff --git a/src/haz3lweb/www/style/assistant.css b/src/haz3lweb/www/style/assistant.css index 2d70d87b33..89ff3300bd 100644 --- a/src/haz3lweb/www/style/assistant.css +++ b/src/haz3lweb/www/style/assistant.css @@ -6,6 +6,7 @@ flex-direction: column; overflow-y: auto; gap: 1em; + overflow-x: hidden; /* Prevent outer scroll */ } #assistant .assistant-button { @@ -595,4 +596,59 @@ #assistant .resuggest-button:hover { opacity: 0.9 !important; +} + +#assistant .example { + overflow-x: scroll !important; + overflow-y: scroll !important; + width: 100%; + min-height: fit-content; + max-height: 400px; + padding: 8px; + border-radius: 4px; + margin: 4px 0; + background: var(--SAND); + box-sizing: border-box; + display: block; + position: relative; /* Create new containing block */ +} + +#assistant .example .code { + display: inline-block; + white-space: nowrap; + min-width: min-content; + padding-bottom: 4px; + position: relative; /* For proper width calculation */ + width: max-content; /* Allow content to determine width */ +} + +#assistant .example .cell-editor { + white-space: pre; + display: inline-block; + position: relative; /* For proper width calculation */ +} + +/* Hide default editor scrollbar */ +#assistant .example .cell-editor::-webkit-scrollbar { + display: none; +} + +/* Style the example scrollbar */ +#assistant .example::-webkit-scrollbar { + height: 8px; +} + +#assistant .example::-webkit-scrollbar-thumb { + background: var(--STONE); + border-radius: 4px; +} + +#assistant .message-code { + overflow-x: auto; + white-space: pre; + max-width: 100%; + padding: 8px; + background: var(--SAND); + border-radius: 4px; + margin: 4px 0; } \ No newline at end of file From ee0bfe2c0d37d710bf7a190e6b903c1f0a0c6a6a Mon Sep 17 00:00:00 2001 From: Cyrus Desai Date: Thu, 6 Mar 2025 02:32:38 -0500 Subject: [PATCH 50/50] fixes bug where resuggesting deletes character to the left --- src/haz3lweb/view/Page.re | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index f5cd2c9b75..fa228285cd 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -280,11 +280,17 @@ module Update = { }; */ print_endline("resuggest: " ++ string_of_bool(resuggest)); - let actions = [ - Action.Select(Tile(Id(tile, Direction.Left))), - Action.Destruct(Direction.Left), - Action.Buffer(Set(LLMSug(response))), - ]; + let actions = + resuggest + ? [ + Action.Select(Tile(Id(tile, Direction.Left))), + Action.Buffer(Set(LLMSug(response))), + ] + : [ + Action.Select(Tile(Id(tile, Direction.Left))), + Action.Destruct(Direction.Left), + Action.Buffer(Set(LLMSug(response))), + ]; // Apply each action in sequence List.iter( action => {