Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ LeetTutor has one product experience with two maintained implementations:
progress, local/cloud providers, and the floating JARVIS;
- trusted-LAN host mode for that same application;
- the browser-hosted application in `web-demo`, which preserves the original
split-workspace and floating-JARVIS UX while connecting directly to Ollama on
`127.0.0.1:11434`. It requires no local Python or Streamlit process.
training controls, split workspace, floating/docked JARVIS, system-design
mode, and provider settings. It requires no local Python or Streamlit process.

Treat `app.py` as the product reference. A hosted UX change must be checked
against the Streamlit behavior, translated in Chinese and English, and usable
Expand All @@ -18,11 +18,13 @@ Streamlit implementations use different rendering runtimes.
## Safety invariants

The public portfolio server must never proxy model traffic. The browser UI may
call only Ollama's loopback API; Caddy must allow the exact 11434 loopback
origins without exposing that port publicly. Hosted Python execution stays in a
bounded Web Worker via Pyodide, and browser drafts/progress stay in local
storage. The legacy browser bridge must retain its loopback bind, reviewed
origins, bounded API allowlists, and loopback-only model upstreams. The
call only the explicitly configured provider directly: reviewed loopback ports
for Ollama, LM Studio, and AMD Metal, or the official OpenAI/Gemini HTTPS API.
Caddy must keep an exact `connect-src` allowlist without exposing local ports
publicly. Cloud API keys are session-only and must never enter local storage.
Hosted Python execution stays in a bounded Web Worker via Pyodide, and browser
drafts/progress stay in local storage. The legacy browser bridge must retain its
loopback bind, reviewed origins, bounded API allowlists, and loopback-only model upstreams. The
Streamlit app must continue to use `leettutor.code_runner` and `SolutionStore`.
Do not store prompts, code, API keys, responses, model files, or progress on EC2.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

## 在线入口 + 本机 Ollama

[tonytan.me/leetcode/](https://tonytan.me/leetcode/) 是完整的浏览器版产品:左右题目/代码工作区、漂浮 JARVIS、学习进度、中英文和明暗主题都直接由主页托管。它不再加载本机 Streamlit,也不要求克隆仓库或启动 Python。
[tonytan.me/leetcode/](https://tonytan.me/leetcode/) 以 `app.py` 为产品规格保留完整浏览器体验:导师训练控制台、左右题目/代码工作区、可停靠或拖动的 JARVIS、真实浏览器 Python 测试、系统设计 Mission Control / JARVIS Live、学习进度、中英文、明暗主题和手机底部导航都由主页直接托管。它不再加载本机 Streamlit,也不要求克隆仓库或启动 Python。

页面会直接检测 `127.0.0.1:11434`:Ollama 已运行时自动读取本机模型;未运行时可通过 `ollama://` 请求系统打开;仍不可用时会区分浏览器权限、`OLLAMA_ORIGINS`、缺少应用和缺少模型,并显示对应操作。Python 测试在浏览器隔离 Worker 中运行;草稿和进度保存在这个浏览器;Prompt 与回答只在浏览器和本机 Ollama 之间传输,不经过 EC2。
页面默认直接检测 `127.0.0.1:11434`:Ollama 已运行时自动读取本机模型;未运行时可通过 `ollama://` 请求系统打开;仍不可用时会区分浏览器权限、`OLLAMA_ORIGINS`、缺少应用和缺少模型,并显示对应操作。左上角 **LT** 模型中心也保留原版的 LM Studio、AMD Metal、OpenAI 和 Gemini Provider;本地端点直接访问 loopback,云端 API Key 只存在当前标签页。Python 测试在浏览器隔离 Worker 中运行;草稿和进度保存在这个浏览器;Prompt 与回答只在浏览器和所选 Provider 之间传输,不经过 EC2。

`app.py`、局域网模式和 `scripts/browser_bridge.py` 仍保留为本地开发、可信局域网和兼容调试入口,但都不是在线版的前置依赖。

Expand Down
4 changes: 2 additions & 2 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ LeetTutor is a local-first AI learning workspace for LeetCode and system design.

## Hosted entry + local Ollama

[tonytan.me/leetcode/](https://tonytan.me/leetcode/) is the complete browser product. The split problem/code workspace, floating JARVIS, progress, bilingual copy, and system/light/dark appearance are hosted on the portfolio itself. It no longer embeds local Streamlit and does not require cloning the repository or starting Python.
[tonytan.me/leetcode/](https://tonytan.me/leetcode/) uses `app.py` as its product specification and preserves the full browser experience: tutor mission controls, the split problem/code workspace, draggable or docked JARVIS, real browser-side Python tests, System Design Mission Control / JARVIS Live, progress, bilingual copy, system/light/dark appearance, and mobile bottom navigation. It no longer embeds local Streamlit and does not require cloning the repository or starting Python.

The page detects `127.0.0.1:11434` directly. It reads local models when Ollama is ready, requests the operating system to open Ollama through `ollama://` when it is stopped, and distinguishes browser permission, `OLLAMA_ORIGINS`, missing-app, and missing-model states when the API remains unavailable. Python tests run in an isolated browser Worker, drafts and progress stay in this browser, and prompts and answers travel only between the browser and local Ollama—not through EC2.
The page detects `127.0.0.1:11434` directly by default. It reads local models when Ollama is ready, requests the operating system to open Ollama through `ollama://` when it is stopped, and distinguishes browser permission, `OLLAMA_ORIGINS`, missing-app, and missing-model states when the API remains unavailable. The **LT** model center also retains the original LM Studio, AMD Metal, OpenAI, and Gemini providers. Local endpoints remain on loopback, and a cloud API key exists only in the current tab. Python tests run in an isolated browser Worker, drafts and progress stay in this browser, and prompts and answers travel only between the browser and the selected provider—not through EC2.

`app.py`, trusted-LAN mode, and `scripts/browser_bridge.py` remain available for local development, private-network access, and compatibility diagnostics, but none is a prerequisite for the hosted product.

Expand Down
49 changes: 47 additions & 2 deletions scripts/export_web_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,62 @@
sys.path.insert(0, str(PROJECT_ROOT))

from leettutor.curriculum import PROBLEMS # noqa: E402
from leettutor.prompts import ( # noqa: E402
ALGORITHM_SYSTEM_PROMPT,
SYSTEM_DESIGN_SYSTEM_PROMPT,
)
from leettutor.system_design_curriculum import SYSTEM_DESIGN_CASES # noqa: E402


PROBLEM_ENGLISH_COPY: dict[int, tuple[str, str]] = {
704: ("Establish a closed search interval and its convergence invariant", "At loop exit, what do left and right represent?"),
35: ("Turn exact lookup into finding the first feasible position", "What condition means mid can still be the answer?"),
34: ("Unify lower and upper bounds without ambiguous equality cases", "With while left < right, what makes an index feasible for the first target?"),
153: ("Use right as a reliable reference while preserving the minimum candidate", "What half can nums[mid] versus nums[right] safely eliminate?"),
33: ("Identify the sorted half and decide whether target lies inside it", "Which half must be sorted on every iteration?"),
875: ("Advance from searching indices to searching the answer space", "As speed increases, how does completion time change monotonically?"),
410: ("Rewrite an optimization problem as a feasibility decision", "Given a maximum segment sum limit, how can you decide whether it is feasible?"),
20: ("Define the stack as the set of unmatched opening tokens", "When a closing bracket arrives, what should be resolved?"),
155: ("Maintain auxiliary state in lockstep with the main stack", "After a pop, how is the previous minimum restored?"),
496: ("Understand which elements are waiting to be resolved in a monotonic stack", "What relation between the stack top and current value should trigger a pop?"),
739: ("Store indices in the stack so distance remains available", "Why is storing temperatures alone insufficient?"),
394: ("Use a stack to preserve the context of each nesting level", "Which pieces of state must be saved before entering a new bracket?"),
84: ("Use a monotonic stack to find the first shorter bar on each side", "When does a bar's complete usable width become known?"),
42: ("Resolve each basin with an explicit height and width", "Geometrically, what does a popped bar represent?"),
215: ("Keep a fixed-size min-heap", "What should the heap top mean among the retained elements?"),
347: ("Combine a frequency table with a fixed-size heap", "Why does a size-k min-heap make eviction straightforward?"),
973: ("State exactly what the heap top and retained set mean", "For a size-k heap, should the top be the nearest or farthest retained point?"),
23: ("Keep only the current candidate from each sorted list", "Why does the heap never need more than k entries?"),
295: ("Maintain partition and size invariants with two heaps", "Which half belongs in each heap, and how far may their sizes differ?"),
70: ("Define a one-dimensional state from the recursive question", "Does dp[i] count ways to reach i or ways remaining after i?"),
198: ("Write the choices to take or skip the current house", "Which two choices produce dp[i]?"),
213: ("Split the cycle into two mutually exclusive linear cases", "Why can the first and last houses never appear together?"),
322: ("Choose an unreachable initial value for minimum-count DP", "Why can the initial array not be all zeroes?"),
518: ("Use iteration order to distinguish combinations from permutations", "Why does iterating coins first avoid recounting orderings?"),
300: ("Distinguish a subsequence ending at i from one using the first i items", "If dp[i] means ending at i, must the answer be dp[-1]?"),
1143: ("Define a two-dimensional prefix state and the character-match choice", "When the last characters differ, which two subproblems should be compared?"),
72: ("Map insert, delete, and replace to coordinate movement", "After one operation, which of i and j decreases?"),
312: ("Reverse the choice by selecting the last balloon to burst", "Why is choosing the last balloon easier for defining boundaries than choosing the first?"),
}


def build_catalog() -> dict[str, object]:
return {
"schema_version": 1,
"schema_version": 2,
"problems": [
{**asdict(problem), "url": f"https://leetcode.com/problems/{problem.slug}/"}
{
**asdict(problem),
"focus_en": PROBLEM_ENGLISH_COPY[problem.id][0],
"invariant_prompt_en": PROBLEM_ENGLISH_COPY[problem.id][1],
"url": f"https://leetcode.com/problems/{problem.slug}/",
}
for problem in PROBLEMS
],
"system_design": [asdict(case) for case in SYSTEM_DESIGN_CASES],
"prompts": {
"algorithm": ALGORITHM_SYSTEM_PROMPT,
"system_design": SYSTEM_DESIGN_SYSTEM_PROMPT,
},
}


Expand Down
25 changes: 25 additions & 0 deletions tests/test_browser_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,31 @@ def test_committed_browser_catalog_matches_python_source(self) -> None:
committed = json.loads((ROOT / "web-demo" / "catalog.json").read_text(encoding="utf-8"))
self.assertEqual(committed, build_catalog())

def test_hosted_ui_keeps_the_original_product_surfaces(self) -> None:
index_source = (ROOT / "web-demo" / "index.html").read_text(encoding="utf-8")
app_source = (ROOT / "web-demo" / "app.js").read_text(encoding="utf-8")
catalog = json.loads((ROOT / "web-demo" / "catalog.json").read_text(encoding="utf-8"))
for element_id in (
"settings-drawer",
"algorithm-session-title",
"algorithm-workspace",
"problem-pane",
"code-pane",
"docked-mentor",
"system-mission-title",
"system-live-content",
"system-command-form",
"mobile-nav",
):
with self.subTest(element_id=element_id):
self.assertIn(f'id="{element_id}"', index_source)
self.assertIn("chooseNextProblem", app_source)
self.assertIn("executePython", app_source)
self.assertIn("askJarvis", app_source)
self.assertIn("renderSystem", app_source)
self.assertTrue(catalog["prompts"]["algorithm"])
self.assertTrue(catalog["prompts"]["system_design"])

def test_accepts_loopback_model_servers(self) -> None:
self.assertEqual(validate_upstream("http://127.0.0.1:11434/"), "http://127.0.0.1:11434")
self.assertEqual(validate_upstream("http://localhost:1234/v1"), "http://localhost:1234/v1")
Expand Down
Loading