上一篇我們把 Chat UI 接上 Agent,加入 streaming,也讓每個 conversation 可以 reuse 自己的 sandbox。
今天補上 frontend 的另外兩個部分:
Frontend 有一個 Data page,讓 user upload 自己的 dataset。
目前的 flow 大概是:
CSV / Parquet
↓
Polars Parse
↓
Preview
↓
Infer Data Types
↓
Save
Upload 之後,backend 會先用 Polars parse,讓 user 看到:
確認之後,再決定怎麼保存。

這裡可以看到除了前面一直使用的 videos dataset,也可以加入 user 自己 upload 的資料,例如 bookstore_sales。
目前有兩種方式:
| Save as | Agent 怎麼使用 |
|---|---|
| Database Table | SQL:query_database / export_query |
| Parquet File | Sandbox 裡直接用 Polars |
兩種方式處理不同需求:
Database Table
→ query
→ filtering
→ aggregation
Parquet File
→ sandbox analysis
→ Polars
→ larger processing
Frontend 做的不只是「上傳檔案」。
真正要做的是把 user upload 的 data 變成 Agent 可以使用的 datasource。
前面幾篇我們一直假設 database 裡只有:
videos
但開始允許 user upload 之後,這個假設就不存在了。
Agent 需要自己知道:
現在有哪些 datasets?
有哪些 columns?
data type 是什麼?
所以加入一個:
list_datasets()
Conceptually:
Agent
↓
list_datasets()
↓
videos
bookstore_sales
uploaded_xxx
這樣新增 dataset 時,不需要一直修改 system prompt。
Agent 可以先 discover 現在有哪些 datasource,再決定下一步怎麼分析。
有 data 之後,接下來就是 result 怎麼呈現。
Agent 現在可以在 sandbox 裡產生:
report.html
例如包含:
有些問題只需要簡單 chart:

而有些 analysis 會產生完整的 HTML report。
Backend execution 結束後,會把 artifact 從 sandbox 拉回來,再透過:
/api/artifacts/...
提供給 frontend。
Chat UI 就可以直接把 report render 在 conversation 裡。

整個 flow 變成:
User Question
↓
Agent
↓
Analysis
↓
Sandbox
↓
report.html
↓
Backend
↓
Frontend
這也開始接近一開始對 Analysis output 的想像。
不是只有:
Agent
→ Markdown Answer
而是:
Agent
→ Analysis
→ HTML Report
HTML 適合這個 project 的原因在於 output 不一定只是文字。
可能同時需要:
Text
Table
Chart
Layout
Interactive Visualization
如果只回 Markdown,presentation ability 會比較受限。
HTML 則可以讓 Agent 根據不同 analysis result,自由組合 report structure。
例如同一個 Analysis 套到不同 dataset:
Same Analysis
+ US Data
→ US Report
Same Analysis
+ Germany Data
→ Germany Report
Report 的 layout 可以類似,但真正的 findings、charts 和 observations 會跟著 data 改變。
所以我們保存的不是一份固定 HTML template。
HTML 是每次 execution 產生的 artifact。
這裡會遇到跟之前很像的問題。
Before:
Untrusted Python
→ 不要直接跑在 Backend
今天則是:
Untrusted HTML / JavaScript
→ 不要直接跑在 Application Origin
因為這份 HTML 是 model-generated HTML。
它可能包含 JavaScript,例如 Plotly。
如果直接跟 application 用同一個 origin 執行:
Model-written JavaScript
↓
Same Origin
↓
可能存取 Storage / Cookies / API
這不是我們想要的。
所以這些 HTML 一樣要被視為 untrusted content。
Frontend 使用:
<iframe
src={artifact.url}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
/>
這裡允許:
allow-scripts
因為 report 裡面的 chart library 可能需要 JavaScript。
但刻意沒有:
allow-same-origin
所以 report 可以執行自己的 script,但不會拿到 application origin 的權限。
Conceptually:
report.html
↓
Sandboxed iframe
↓
Can run its own JS
↓
Cannot act as the application
只靠 iframe 還不夠。
因為 user 也可能直接點:
Open ↗
把 artifact 單獨打開。
所以 backend 回傳 artifact 時,也加上:
Content-Security-Policy: sandbox allow-scripts
X-Content-Type-Options: nosniff
變成兩層:
Inside App
→ iframe sandbox
Direct Open
→ CSP sandbox
這樣即使 report 被單獨打開,本身還是 isolated。

Agent 的一般回答不需要支援 arbitrary HTML。
所以 normal answer 走:
Agent Text
→ react-markdown
而且不 render raw HTML。
例如 Agent 回:
<img src=x onerror="...">
Frontend 不會直接把它當成 executable HTML。
HTML report 則走另外一條 path:
Agent Text
→ Markdown Renderer
Agent Report
→ Sandboxed iframe
我會把這兩種 content 分開處理,而不是全部丟給同一個 renderer。
前一篇我們處理的是:
Agent-generated Python
→ Execution Sandbox
今天處理的是:
Agent-generated HTML
→ Browser Sandbox
背後其實是同一件事:
Model-generated content 不應該因為「是 Agent 產生的」就被當成 trusted content。
到這裡,user flow 已經開始完整:
Upload Data
↓
Discover Dataset
↓
Chat with Agent
↓
Sandbox Analysis
↓
Generate HTML Report
↓
Render in Frontend
Frontend 本身不是這個系列的核心。
它的作用是把前面做好的:
Datasource
Agent
Skills
Sandbox
Artifacts
組成一個 user 真正可以操作的 workflow。
現在 user 已經可以:
Upload Data
→ Ask Question
→ Run Analysis
→ See Report