summaryrefslogtreecommitdiff
path: root/Omni/Ava/Web.hs
diff options
context:
space:
mode:
Diffstat (limited to 'Omni/Ava/Web.hs')
-rw-r--r--Omni/Ava/Web.hs98
1 files changed, 89 insertions, 9 deletions
diff --git a/Omni/Ava/Web.hs b/Omni/Ava/Web.hs
index 4d4ece6..f9dd7cd 100644
--- a/Omni/Ava/Web.hs
+++ b/Omni/Ava/Web.hs
@@ -26,13 +26,11 @@ import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
-import qualified Data.Text.IO as TextIO
import qualified Database.SQLite.Simple as SQL
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import qualified Omni.Ava.Trace as Trace
-import qualified System.Environment as Environment
main :: IO ()
main = putText "Use Omni.Ava for the main entry point"
@@ -75,15 +73,97 @@ serveTraceJson dbPath tid _req respond = do
respond <| Wai.responseLBS HTTP.status200 [(HTTP.hContentType, "application/json")] json
renderTraceHtml :: Trace.TraceRecord -> IO Text
-renderTraceHtml rec = do
- coderoot <- Environment.getEnv "CODEROOT"
- let templatePath = coderoot <> "/Omni/Ava/Web/trace.html"
- template <- TextIO.readFile templatePath
+renderTraceHtml rec =
pure
<| Text.replace "{{trace_id}}" (Trace.trcId rec)
<| Text.replace "{{tool_name}}" (Trace.trcToolName rec)
<| Text.replace "{{created_at}}" (Trace.trcCreatedAt rec)
<| Text.replace "{{duration_ms}}" (tshow <| Trace.trcDurationMs rec)
- <| Text.replace "{{input_json}}" (Trace.trcInput rec)
- <| Text.replace "{{output_json}}" (Trace.trcOutput rec)
- <| template
+ <| Text.replace "{{input_json}}" (escapeHtml <| Trace.trcInput rec)
+ <| Text.replace "{{output_json}}" (escapeHtml <| Trace.trcOutput rec)
+ <| traceTemplate
+
+-- | Escape HTML special characters
+escapeHtml :: Text -> Text
+escapeHtml =
+ Text.replace "&" "&amp;"
+ <. Text.replace "<" "&lt;"
+ <. Text.replace ">" "&gt;"
+ <. Text.replace "\"" "&quot;"
+
+-- | Embedded trace HTML template
+traceTemplate :: Text
+traceTemplate =
+ Text.unlines
+ [ "<!DOCTYPE html>",
+ "<html lang=\"en\">",
+ "<head>",
+ " <meta charset=\"utf-8\">",
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
+ " <title>Trace: {{tool_name}}</title>",
+ " <style>",
+ " :root { --bg: #1a1a2e; --fg: #eee; --accent: #4a9eff; --code-bg: #0d0d1a; }",
+ " @media (prefers-color-scheme: light) {",
+ " :root { --bg: #fff; --fg: #222; --accent: #0066cc; --code-bg: #f5f5f5; }",
+ " }",
+ " * { box-sizing: border-box; }",
+ " body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--fg); padding: 1rem; margin: 0; max-width: 100%; }",
+ " h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }",
+ " .meta { font-size: 0.875rem; opacity: 0.7; margin-bottom: 1rem; }",
+ " .section { margin: 1rem 0; border: 1px solid var(--accent); border-radius: 8px; overflow: hidden; }",
+ " .section-header { padding: 0.75rem 1rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; background: var(--code-bg); }",
+ " .section-header::before { content: '▼'; margin-right: 0.5rem; transition: transform 0.2s; }",
+ " .section.collapsed .section-header::before { transform: rotate(-90deg); }",
+ " .section-content { padding: 1rem; display: block; overflow-x: auto; background: var(--code-bg); }",
+ " .section.collapsed .section-content { display: none; }",
+ " pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 0.875rem; font-family: ui-monospace, monospace; }",
+ " .copy-btn { background: var(--accent); color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; }",
+ " .copy-btn:hover { opacity: 0.8; }",
+ " .footer { font-size: 0.75rem; opacity: 0.5; margin-top: 2rem; text-align: center; }",
+ " </style>",
+ "</head>",
+ "<body>",
+ " <h1>{{tool_name}}</h1>",
+ " <p class=\"meta\">{{created_at}} · {{duration_ms}}ms</p>",
+ "",
+ " <div class=\"section\" id=\"input-section\">",
+ " <div class=\"section-header\">",
+ " Input",
+ " <button class=\"copy-btn\" data-target=\"input-content\">Copy</button>",
+ " </div>",
+ " <div class=\"section-content\" id=\"input-content\"><pre>{{input_json}}</pre></div>",
+ " </div>",
+ "",
+ " <div class=\"section\" id=\"output-section\">",
+ " <div class=\"section-header\">",
+ " Output",
+ " <button class=\"copy-btn\" data-target=\"output-content\">Copy</button>",
+ " </div>",
+ " <div class=\"section-content\" id=\"output-content\"><pre>{{output_json}}</pre></div>",
+ " </div>",
+ "",
+ " <p class=\"footer\">Trace ID: {{trace_id}}</p>",
+ "",
+ " <script>",
+ " document.querySelectorAll('.section-header').forEach(header => {",
+ " header.addEventListener('click', (e) => {",
+ " if (e.target.classList.contains('copy-btn')) return;",
+ " header.parentElement.classList.toggle('collapsed');",
+ " });",
+ " });",
+ "",
+ " document.querySelectorAll('.copy-btn').forEach(btn => {",
+ " btn.addEventListener('click', () => {",
+ " const targetId = btn.getAttribute('data-target');",
+ " const text = document.getElementById(targetId).querySelector('pre').textContent;",
+ " navigator.clipboard.writeText(text).then(() => {",
+ " const orig = btn.textContent;",
+ " btn.textContent = 'Copied!';",
+ " setTimeout(() => btn.textContent = orig, 1500);",
+ " });",
+ " });",
+ " });",
+ " </script>",
+ "</body>",
+ "</html>"
+ ]