← Back to task

Commit 717fd149

commit 717fd14986fea45f2a539068be67a7f132cdedad
Author: Ben Sima <ben@bensima.com>
Date:   Fri Nov 28 02:08:50 2025

    Add create fact form in web UI
    
    All tests pass. The create fact form has been added to the web
    UI. Here'
    
    1. **Added `FactCreateForm` data type** - New form type to handle the
    cr 2. **Added API route** - `POST /kb/create` endpoint that accepts
    the for 3. **Added handler** - `factCreateHandler` that creates a
    new fact using 4. **Added form UI** - A collapsible form on the KB
    page with fields for
       - Project (required) - Fact Content (required textarea) - Related
       Files (optional, comma-separated) - Confidence level (0.0-1.0,
       default 0.8)
    5. **Added CSS styles** - Styling for the create fact section in
    both li
    
    Task-Id: t-158.6

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index eb8a7518..e58992c8 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -62,6 +62,7 @@ type API =
       :> QueryParam "type" Text
       :> Get '[Lucid.HTML] TaskListPage
     :<|> "kb" :> Get '[Lucid.HTML] KBPage
+    :<|> "kb" :> "create" :> ReqBody '[FormUrlEncoded] FactCreateForm :> PostRedirect
     :<|> "kb" :> Capture "id" Int :> Get '[Lucid.HTML] FactDetailPage
     :<|> "kb" :> Capture "id" Int :> "edit" :> ReqBody '[FormUrlEncoded] FactEditForm :> PostRedirect
     :<|> "kb" :> Capture "id" Int :> "delete" :> PostRedirect
@@ -148,6 +149,16 @@ instance FromForm FactEditForm where
     let confidence = fromRight "0.8" (lookupUnique "confidence" form)
     Right (FactEditForm content files confidence)
 
+data FactCreateForm = FactCreateForm Text Text Text Text
+
+instance FromForm FactCreateForm where
+  fromForm form = do
+    project <- parseUnique "project" form
+    content <- parseUnique "content" form
+    let files = fromRight "" (lookupUnique "files" form)
+    let confidence = fromRight "0.8" (lookupUnique "confidence" form)
+    Right (FactCreateForm project content files confidence)
+
 data EpicsPage = EpicsPage [TaskCore.Task] [TaskCore.Task]
 
 newtype RecentActivityPartial = RecentActivityPartial [TaskCore.Task]
@@ -570,6 +581,60 @@ instance Lucid.ToHtml KBPage where
         Lucid.div_ [Lucid.class_ "container"] <| do
           Lucid.h1_ "Knowledge Base"
           Lucid.p_ [Lucid.class_ "info-msg"] "Facts learned during task execution."
+
+          Lucid.details_ [Lucid.class_ "create-fact-section"] <| do
+            Lucid.summary_ [Lucid.class_ "btn btn-primary create-fact-toggle"] "Create New Fact"
+            Lucid.form_
+              [ Lucid.method_ "POST",
+                Lucid.action_ "/kb/create",
+                Lucid.class_ "fact-create-form"
+              ]
+              <| do
+                Lucid.div_ [Lucid.class_ "form-group"] <| do
+                  Lucid.label_ [Lucid.for_ "project"] "Project:"
+                  Lucid.input_
+                    [ Lucid.type_ "text",
+                      Lucid.name_ "project",
+                      Lucid.id_ "project",
+                      Lucid.class_ "form-input",
+                      Lucid.required_ "required",
+                      Lucid.placeholder_ "e.g., Omni/Jr"
+                    ]
+                Lucid.div_ [Lucid.class_ "form-group"] <| do
+                  Lucid.label_ [Lucid.for_ "content"] "Fact Content:"
+                  Lucid.textarea_
+                    [ Lucid.name_ "content",
+                      Lucid.id_ "content",
+                      Lucid.class_ "form-textarea",
+                      Lucid.rows_ "4",
+                      Lucid.required_ "required",
+                      Lucid.placeholder_ "Describe the fact or knowledge..."
+                    ]
+                    ""
+                Lucid.div_ [Lucid.class_ "form-group"] <| do
+                  Lucid.label_ [Lucid.for_ "files"] "Related Files (comma-separated):"
+                  Lucid.input_
+                    [ Lucid.type_ "text",
+                      Lucid.name_ "files",
+                      Lucid.id_ "files",
+                      Lucid.class_ "form-input",
+                      Lucid.placeholder_ "path/to/file1.hs, path/to/file2.hs"
+                    ]
+                Lucid.div_ [Lucid.class_ "form-group"] <| do
+                  Lucid.label_ [Lucid.for_ "confidence"] "Confidence (0.0 - 1.0):"
+                  Lucid.input_
+                    [ Lucid.type_ "number",
+                      Lucid.name_ "confidence",
+                      Lucid.id_ "confidence",
+                      Lucid.class_ "form-input",
+                      Lucid.step_ "0.1",
+                      Lucid.min_ "0",
+                      Lucid.max_ "1",
+                      Lucid.value_ "0.8"
+                    ]
+                Lucid.div_ [Lucid.class_ "form-actions"] <| do
+                  Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Create Fact"
+
           if null facts
             then Lucid.p_ [Lucid.class_ "empty-msg"] "No facts recorded yet."
             else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderFactCard facts
@@ -1614,6 +1679,7 @@ server =
     :<|> statsHandler
     :<|> taskListHandler
     :<|> kbHandler
+    :<|> factCreateHandler
     :<|> factDetailHandler
     :<|> factEditHandler
     :<|> factDeleteHandler
@@ -1682,6 +1748,13 @@ server =
       facts <- liftIO Fact.getAllFacts
       pure (KBPage facts)
 
+    factCreateHandler :: FactCreateForm -> Servant.Handler (Headers '[Header "Location" Text] NoContent)
+    factCreateHandler (FactCreateForm project content filesText confText) = do
+      let files = filter (not <. Text.null) (Text.splitOn "," (Text.strip filesText))
+          confidence = fromMaybe 0.8 (readMaybe (Text.unpack confText))
+      fid <- liftIO (Fact.createFact project content files Nothing confidence)
+      pure <| addHeader ("/kb/" <> tshow fid) NoContent
+
     factDetailHandler :: Int -> Servant.Handler FactDetailPage
     factDetailHandler fid = do
       maybeFact <- liftIO (Fact.getFact fid)
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index b262037d..7d6e7d65 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -789,6 +789,17 @@ formStyles = do
     padding (px 16) (px 16) (px 16) (px 16)
     borderRadius (px 4) (px 4) (px 4) (px 4)
     border (px 1) solid "#fecaca"
+  ".create-fact-section" ? do
+    marginBottom (px 16)
+  ".create-fact-toggle" ? do
+    cursor pointer
+    display inlineBlock
+  ".fact-create-form" ? do
+    marginTop (px 12)
+    padding (px 16) (px 16) (px 16) (px 16)
+    backgroundColor white
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+    border (px 1) solid "#d1d5db"
 
 executionDetailsStyles :: Css
 executionDetailsStyles = do
@@ -1274,6 +1285,9 @@ darkModeStyles =
       backgroundColor "#374151"
       borderColor "#4b5563"
       color "#f3f4f6"
+    ".fact-create-form" ? do
+      backgroundColor "#1f2937"
+      borderColor "#374151"
     -- Responsive dark mode: dropdown content needs background on mobile
     query Media.screen [Media.maxWidth (px 600)] <| do
       ".navbar-dropdown-content" ? do