jac-coder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. jac_coder-0.1.0.dist-info/top_level.txt +1 -0
jac_coder/__init__.jac ADDED
File without changes
jac_coder/api.jac ADDED
@@ -0,0 +1,82 @@
1
+ """JacCoder public API."""
2
+
3
+ import os;
4
+ import sys;
5
+
6
+ import from datetime { datetime }
7
+ import from jac_coder.config {
8
+ get_config,
9
+ set_model as _set_model,
10
+ get_model as _get_model
11
+ }
12
+ import from jac_coder.util.tool_output { tool_end }
13
+ import from jac_coder.permission { permission_engine }
14
+ import from jac_coder.util.sandbox { set_sandbox_root }
15
+ import from jac_coder.context { build_context, ContextConfig }
16
+ import from jac_coder.walkers { new_session, ensure_main_agent, _consume_llm_stream }
17
+ import from jac_coder.mcp_manager {
18
+ mcp_add_server,
19
+ mcp_remove_server,
20
+ mcp_list_servers,
21
+ mcp_get_tools,
22
+ mcp_register_builtin
23
+ }
24
+ import from jac_coder.memory {
25
+ find_or_create_memory,
26
+ _init_memory,
27
+ update_memory_from_session
28
+ }
29
+ import from jac_coder.nodes { Session, MainAgent }
30
+ import from jac_coder.events {
31
+ register_event_callback,
32
+ clear_event_callbacks,
33
+ reset_steps,
34
+ start_turn,
35
+ emit_turn_summary,
36
+ emit_event
37
+ }
38
+
39
+
40
+ # Thread-safe session registry — graph root() is per-request in jac-cloud,
41
+ # so background threads can't find sessions via [root()-->]. This dict
42
+ # is module-level and accessible from any thread.
43
+ glob _session_registry: dict = {};
44
+
45
+ """Initialize jac-coder."""
46
+ def initialize(mode: str = "web") -> None;
47
+
48
+ """Create a new session."""
49
+ def create_session(directory: str, title: str = "", agent: str = "main") -> dict;
50
+
51
+ """Send a message and get a response."""
52
+ def chat(
53
+ session_id: str,
54
+ message: str,
55
+ directory: str = "",
56
+ on_event: Any = None,
57
+ mode: str = "full",
58
+ agent_context: str = ""
59
+ ) -> dict;
60
+
61
+ def list_sessions() -> list;
62
+
63
+ def get_session(session_id: str) -> dict;
64
+
65
+ def close_session(session_id: str) -> dict;
66
+
67
+ # --- Model management API ---
68
+ """Switch the active LLM model."""
69
+ def api_set_model(model: str) -> dict;
70
+
71
+ """Get current model info."""
72
+ def api_get_model() -> dict;
73
+
74
+ # --- MCP management API ---
75
+ """Add or update an MCP server."""
76
+ def api_mcp_add(name: str, config: dict) -> dict;
77
+
78
+ """Remove an MCP server."""
79
+ def api_mcp_remove(name: str) -> dict;
80
+
81
+ """List all configured MCP servers."""
82
+ def api_mcp_list() -> list;
jac_coder/cli_entry.py ADDED
@@ -0,0 +1,25 @@
1
+ """CLI entry point for `jac-coder` command."""
2
+
3
+ import subprocess
4
+ import sys
5
+ import os
6
+
7
+
8
+ def main():
9
+ # Find the project root (where cli.jac lives)
10
+ pkg_dir = os.path.dirname(os.path.abspath(__file__))
11
+ project_root = os.path.dirname(pkg_dir)
12
+ cli_jac = os.path.join(project_root, "cli.jac")
13
+
14
+ if not os.path.exists(cli_jac):
15
+ print("Error: cli.jac not found. Run from the jac-coder project directory.")
16
+ sys.exit(1)
17
+
18
+ # Set JACCODER_ROOT so tools can find data/ directory regardless of CWD
19
+ os.environ["JACCODER_ROOT"] = project_root
20
+
21
+ sys.exit(subprocess.call(["jac", cli_jac] + sys.argv[1:]))
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
jac_coder/config.jac ADDED
@@ -0,0 +1,36 @@
1
+ import os;
2
+ import json;
3
+
4
+ import from pathlib { Path }
5
+ import from dotenv { load_dotenv }
6
+ import from byllm.lib { Model }
7
+
8
+
9
+ obj JacCoderConfig {
10
+ has default_model: str = "gpt-5.2-2025-12-11",
11
+ temperature: float = 0.2,
12
+ max_tokens: int = 8192,
13
+ max_react_iterations: int = 30,
14
+ project_dir: str = "";
15
+ }
16
+
17
+ glob _config: JacCoderConfig = JacCoderConfig();
18
+
19
+ # --- Config ---
20
+ def get_home_dir() -> str;
21
+ def _merge_from_file(config: JacCoderConfig, path: str) -> None;
22
+ def load_config(project_dir: str = "") -> JacCoderConfig;
23
+ def get_config() -> JacCoderConfig;
24
+ def get_data_dir() -> str;
25
+
26
+ # --- Model management ---
27
+ def set_model(model: str) -> dict;
28
+ def get_model() -> dict;
29
+
30
+ with entry {
31
+ load_dotenv(override=True);
32
+ load_config();
33
+ }
34
+
35
+ glob model_name: str = _config.default_model;
36
+ glob llm = Model(model_name=model_name);
jac_coder/context.jac ADDED
@@ -0,0 +1,17 @@
1
+ obj ContextConfig {
2
+ has token_budget: int = 20000;
3
+ has recent_window: int = 8;
4
+ }
5
+
6
+
7
+ def estimate_tokens(messages: list[dict]) -> int;
8
+
9
+ def needs_compaction(messages: list[dict], budget: int = 20000) -> bool;
10
+
11
+ def build_context(
12
+ chat_history: list[dict],
13
+ active_files: list[str] = [],
14
+ pending_errors: list[str] = [],
15
+ config: ContextConfig | None = None,
16
+ project_summary: str = ""
17
+ ) -> list[dict];
@@ -0,0 +1,90 @@
1
+ # Example: AI Agent with LLM Routing
2
+
3
+ Uses `by llm()` for intelligent routing, `sem` annotations for prompts, walker graph traversal.
4
+
5
+ ## Backend: agent.jac — Declarations
6
+
7
+ ```jac
8
+ """AI agent with LLM-driven routing between handler nodes."""
9
+
10
+ import from byllm.lib { Model }
11
+
12
+ glob llm: Model = Model(model_name="gpt-4.1-mini");
13
+
14
+ node Router {}
15
+
16
+ node QAHandler {
17
+ def respond(message: str, context: list[dict]) -> str by llm(
18
+ method="Reason",
19
+ messages=context,
20
+ temperature=0.3
21
+ );
22
+ can handle with process_request entry;
23
+ }
24
+
25
+ node CodeHandler {
26
+ def respond(message: str, context: list[dict]) -> str by llm(
27
+ messages=context,
28
+ tools=[search_docs]
29
+ );
30
+ can handle with process_request entry;
31
+ }
32
+
33
+ walker :pub process_request {
34
+ has message: str = "";
35
+ has context: list[dict] = [];
36
+
37
+ can route with Router entry {
38
+ visit [-->] by llm(
39
+ incl_info={"message": self.message, "context": self.context[-5:]}
40
+ );
41
+ }
42
+
43
+ can init_graph with Root entry {
44
+ visit [-->][?:Router] else {
45
+ router = (here ++> Router())[0];
46
+ router ++> QAHandler();
47
+ router ++> CodeHandler();
48
+ visit router;
49
+ };
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Implementation: impl/agent.impl.jac
55
+
56
+ ```jac
57
+ sem Router.classify = """Return exactly one label: QA, CODE, or DONE.
58
+ QA = general questions, CODE = programming tasks, DONE = task complete.""";
59
+
60
+ sem QAHandler.respond = """You are a helpful assistant. Answer clearly and concisely.""";
61
+
62
+ sem CodeHandler.respond = """You are an expert programmer.
63
+ Use search_docs to find documentation before answering.""";
64
+
65
+ impl QAHandler.handle with process_request entry {
66
+ response = here.respond(message=self.message, context=self.context);
67
+ report {"response": response, "handler": "qa"};
68
+ visit [<--][?:Router];
69
+ }
70
+
71
+ impl CodeHandler.handle with process_request entry {
72
+ response = here.respond(message=self.message, context=self.context);
73
+ report {"response": response, "handler": "code"};
74
+ visit [<--][?:Router];
75
+ }
76
+
77
+ impl process_request.route with Router entry {
78
+ label = here.classify(message=self.message, context=self.context);
79
+ if "DONE" in label { disengage; }
80
+ }
81
+ ```
82
+
83
+ ## Key Patterns Demonstrated
84
+
85
+ - **Lazy graph creation**: `visit [-->][?:Router] else { ... }` creates graph on first use
86
+ - **LLM-driven routing**: `visit [-->] by llm(...)` lets LLM choose which handler
87
+ - **Interface/impl split**: Declarations in `.jac`, semantics + logic in `.impl.jac`
88
+ - **Multi-node traversal**: Walker visits Router → Handler → back to Router
89
+ - **`sem` annotations**: System prompts for each LLM-powered method
90
+ - **`disengage`**: Stops walker when task is complete
@@ -0,0 +1,386 @@
1
+ # Example: Blog with Posts & Comments
2
+
3
+ Fullstack blog with related data models (Post → Comment), walker endpoint, service layer, dynamic routing.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ blog-app/
9
+ ├── jac.toml
10
+ ├── main.jac # ALL backend + cl { app() }
11
+ ├── services/apiService.cl.jac # Service layer (error handling)
12
+ ├── hooks/
13
+ │ ├── usePosts.cl.jac # Posts hook
14
+ │ └── useComments.cl.jac # Comments hook
15
+ ├── components/
16
+ │ ├── Layout.cl.jac # Root layout with Outlet
17
+ │ ├── Header.cl.jac # Nav bar
18
+ │ ├── PostCard.cl.jac # Post summary
19
+ │ ├── PostForm.cl.jac # Create post form
20
+ │ └── CommentList.cl.jac # Comments + add form
21
+ ├── pages/
22
+ │ ├── layout.jac # Root nav
23
+ │ ├── index.jac # Home — post list
24
+ │ └── posts/[id].jac # Single post (dynamic)
25
+ └── styles/global.css
26
+ ```
27
+
28
+ ## main.jac — Backend (Related Nodes + Walker)
29
+
30
+ ```jac
31
+ import from uuid { uuid4 }
32
+ import from datetime { datetime }
33
+
34
+ node Post {
35
+ has id: str = "";
36
+ has title: str = "";
37
+ has body: str = "";
38
+ has author: str = "";
39
+ has category: str = "";
40
+ has created_at: str = "";
41
+ }
42
+
43
+ node Comment {
44
+ has id: str = "";
45
+ has author: str = "";
46
+ has text: str = "";
47
+ has created_at: str = "";
48
+ }
49
+
50
+ # --- Post CRUD ---
51
+
52
+ def:pub get_posts(category: str = "") -> list {
53
+ all_posts = [root-->][?:Post];
54
+ if category {
55
+ all_posts = [p for p in all_posts if p.category == category];
56
+ }
57
+ return [
58
+ {"id": p.id, "title": p.title, "body": p.body,
59
+ "author": p.author, "category": p.category,
60
+ "comment_count": len([p-->][?:Comment])}
61
+ for p in all_posts
62
+ ];
63
+ }
64
+
65
+ def:pub get_post(post_id: str) -> dict {
66
+ for p in [root-->][?:Post] {
67
+ if p.id == post_id {
68
+ return {"id": p.id, "title": p.title, "body": p.body,
69
+ "author": p.author, "category": p.category};
70
+ }
71
+ }
72
+ return {"error": "Post not found"};
73
+ }
74
+
75
+ def:pub create_post(title: str, body: str, author: str = "", category: str = "") -> dict {
76
+ post = (root ++> Post(
77
+ id=str(uuid4()), title=title, body=body,
78
+ author=author, category=category,
79
+ created_at=datetime.now().isoformat()
80
+ ))[0];
81
+ return {"id": post.id, "title": post.title};
82
+ }
83
+
84
+ def:pub delete_post(post_id: str) -> dict {
85
+ for p in [root-->][?:Post] {
86
+ if p.id == post_id {
87
+ for c in [p-->][?:Comment] { p del--> c; }
88
+ root del--> p;
89
+ return {"success": True};
90
+ }
91
+ }
92
+ return {"success": False, "error": "Not found"};
93
+ }
94
+
95
+ # --- Comment CRUD (child nodes of Post) ---
96
+
97
+ def:pub get_comments(post_id: str) -> list {
98
+ for p in [root-->][?:Post] {
99
+ if p.id == post_id {
100
+ return [{"id": c.id, "author": c.author, "text": c.text}
101
+ for c in [p-->][?:Comment]];
102
+ }
103
+ }
104
+ return [];
105
+ }
106
+
107
+ def:pub add_comment(post_id: str, author: str, text: str) -> dict {
108
+ for p in [root-->][?:Post] {
109
+ if p.id == post_id {
110
+ comment = (p ++> Comment(
111
+ id=str(uuid4()), author=author, text=text,
112
+ created_at=datetime.now().isoformat()
113
+ ))[0];
114
+ return {"id": comment.id, "author": comment.author, "text": comment.text};
115
+ }
116
+ }
117
+ return {"error": "Post not found"};
118
+ }
119
+
120
+ # --- Walker endpoint (single-request post + comments) ---
121
+
122
+ walker :pub get_post_with_comments {
123
+ has post_id: str = "";
124
+
125
+ can find_post with Root entry {
126
+ for p in [-->][?:Post] {
127
+ if p.id == self.post_id { visit p; return; }
128
+ }
129
+ report {"error": "Post not found"};
130
+ }
131
+
132
+ can collect with Post entry {
133
+ comments = [{"id": c.id, "author": c.author, "text": c.text}
134
+ for c in [-->][?:Comment]];
135
+ report {
136
+ "post": {"id": here.id, "title": here.title, "body": here.body},
137
+ "comments": comments
138
+ };
139
+ }
140
+ }
141
+
142
+ cl import from .components.Layout { Layout }
143
+ cl {
144
+ def:pub app() -> JsxElement { return <Layout />; }
145
+ }
146
+ ```
147
+
148
+ ## services/apiService.cl.jac — Service Layer
149
+
150
+ ```jac
151
+ sv import from ..main { get_posts, get_post, create_post, delete_post,
152
+ get_comments, add_comment, get_post_with_comments }
153
+
154
+ async def:pub fetchPosts(category: str = "") -> any {
155
+ try {
156
+ posts = await get_posts(category);
157
+ return {"success": True, "posts": posts or []};
158
+ } except Exception as e {
159
+ return {"success": False, "error": str(e), "posts": []};
160
+ }
161
+ }
162
+
163
+ async def:pub fetchPost(postId: str) -> any {
164
+ try {
165
+ post = await get_post(postId);
166
+ if post and not post.error {
167
+ return {"success": True, "post": post};
168
+ }
169
+ return {"success": False, "error": post.error or "Not found"};
170
+ } except Exception as e {
171
+ return {"success": False, "error": str(e)};
172
+ }
173
+ }
174
+
175
+ async def:pub submitPost(title: str, body: str, author: str = "", category: str = "") -> any {
176
+ try {
177
+ result = await create_post(title, body, author, category);
178
+ return {"success": True, "post": result};
179
+ } except Exception as e {
180
+ return {"success": False, "error": str(e)};
181
+ }
182
+ }
183
+
184
+ async def:pub fetchComments(postId: str) -> any {
185
+ try {
186
+ comments = await get_comments(postId);
187
+ return {"success": True, "comments": comments or []};
188
+ } except Exception as e {
189
+ return {"success": False, "error": str(e), "comments": []};
190
+ }
191
+ }
192
+
193
+ async def:pub submitComment(postId: str, author: str, text: str) -> any {
194
+ try {
195
+ result = await add_comment(postId, author, text);
196
+ if result and not result.error {
197
+ return {"success": True, "comment": result};
198
+ }
199
+ return {"success": False, "error": result.error or "Failed"};
200
+ } except Exception as e {
201
+ return {"success": False, "error": str(e)};
202
+ }
203
+ }
204
+ ```
205
+
206
+ ## hooks/usePosts.cl.jac
207
+
208
+ ```jac
209
+ import from ..services.apiService { fetchPosts, submitPost }
210
+
211
+ def:pub usePosts() -> dict {
212
+ has posts: list = [];
213
+ has isLoading: bool = True;
214
+ has error: str = "";
215
+ has selectedCategory: str = "";
216
+
217
+ async can with entry { await loadPosts(); }
218
+ async can with [selectedCategory] entry { await loadPosts(); }
219
+
220
+ async def loadPosts() -> None {
221
+ isLoading = True;
222
+ error = "";
223
+ result = await fetchPosts(selectedCategory);
224
+ if result.success { posts = result.posts; }
225
+ else { error = result.error or "Failed"; }
226
+ isLoading = False;
227
+ }
228
+
229
+ async def handleCreate(title: str, body: str, category: str = "") -> bool {
230
+ result = await submitPost(title, body, "", category);
231
+ if result.success { await loadPosts(); return True; }
232
+ return False;
233
+ }
234
+
235
+ def setCategory(cat: str) -> None { selectedCategory = cat; }
236
+
237
+ return {
238
+ "posts": posts, "isLoading": isLoading, "error": error,
239
+ "selectedCategory": selectedCategory,
240
+ "setCategory": setCategory, "handleCreate": handleCreate
241
+ };
242
+ }
243
+ ```
244
+
245
+ ## hooks/useComments.cl.jac
246
+
247
+ ```jac
248
+ import from ..services.apiService { fetchComments, submitComment }
249
+
250
+ def:pub useComments(postId: str) -> dict {
251
+ has comments: list = [];
252
+ has isLoading: bool = True;
253
+
254
+ async can with entry { await loadComments(); }
255
+
256
+ async def loadComments() -> None {
257
+ isLoading = True;
258
+ result = await fetchComments(postId);
259
+ if result.success { comments = result.comments; }
260
+ isLoading = False;
261
+ }
262
+
263
+ async def handleAdd(author: str, text: str) -> bool {
264
+ result = await submitComment(postId, author, text);
265
+ if result.success {
266
+ comments = comments + [result.comment];
267
+ return True;
268
+ }
269
+ return False;
270
+ }
271
+
272
+ return {"comments": comments, "isLoading": isLoading, "handleAdd": handleAdd};
273
+ }
274
+ ```
275
+
276
+ ## components/PostCard.cl.jac
277
+
278
+ ```jac
279
+ cl import from "@jac/runtime" { Link }
280
+
281
+ def:pub PostCard(props: dict) -> JsxElement {
282
+ post = props.post or {};
283
+ onDelete = props.onDelete or None;
284
+ postId = post["id"] or "";
285
+ title = post["title"] or "Untitled";
286
+ body = post["body"] or "";
287
+ category = post["category"] or "";
288
+ commentCount = post["comment_count"] or 0;
289
+
290
+ preview = body;
291
+ if len(body) > 150 { preview = body[0:150] + "..."; }
292
+
293
+ def handle_delete() -> None { onDelete(postId); }
294
+
295
+ return <div className="border rounded-xl p-5 mb-4 bg-white">
296
+ <Link to={"/posts/" + postId} className="text-xl font-semibold">{title}</Link>
297
+ {category and (<span className="ml-2 text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded-full">{category}</span>)}
298
+ <p className="text-gray-500 mt-2">{preview}</p>
299
+ <div className="flex justify-between items-center mt-3 text-sm text-gray-400">
300
+ <span>{str(commentCount) + " comments"}</span>
301
+ {onDelete and (<button onClick={handle_delete} className="text-red-500">Delete</button>)}
302
+ </div>
303
+ </div>;
304
+ }
305
+ ```
306
+
307
+ ## components/CommentList.cl.jac
308
+
309
+ ```jac
310
+ import from ..hooks.useComments { useComments }
311
+
312
+ def:pub CommentList(props: dict) -> JsxElement {
313
+ postId = props.postId or "";
314
+ data = useComments(postId);
315
+ comments = data["comments"] or [];
316
+
317
+ has newAuthor: str = "";
318
+ has newText: str = "";
319
+
320
+ async def handleSubmit(e: any) -> None {
321
+ e.preventDefault();
322
+ if not newAuthor.strip() or not newText.strip() { return; }
323
+ success = await data["handleAdd"](newAuthor.strip(), newText.strip());
324
+ if success { newAuthor = ""; newText = ""; }
325
+ }
326
+
327
+ def handle_author(e: any) -> None { newAuthor = e.target.value; }
328
+ def handle_text(e: any) -> None { newText = e.target.value; }
329
+
330
+ return <div className="mt-6">
331
+ <h3 className="font-semibold mb-3">{"Comments (" + str(len(comments)) + ")"}</h3>
332
+ {data["isLoading"] and (<p className="text-gray-400">Loading...</p>)}
333
+ {[
334
+ <div key={c["id"]} className="p-3 bg-gray-50 rounded mb-2">
335
+ <strong>{c["author"]}</strong>
336
+ <p className="text-gray-600 mt-1">{c["text"]}</p>
337
+ </div>
338
+ for c in comments
339
+ ]}
340
+ <form onSubmit={handleSubmit} className="flex gap-2 mt-3">
341
+ <input value={newAuthor} onChange={handle_author} placeholder="Name" className="px-3 py-2 border rounded w-28" />
342
+ <input value={newText} onChange={handle_text} placeholder="Comment..." className="px-3 py-2 border rounded flex-1" />
343
+ <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">Post</button>
344
+ </form>
345
+ </div>;
346
+ }
347
+ ```
348
+
349
+ ## pages/posts/[id].jac — Dynamic Route
350
+
351
+ ```jac
352
+ cl import from "@jac/runtime" { useParams, Link }
353
+ import from ...services.apiService { fetchPost }
354
+ import from ...components.CommentList { CommentList }
355
+
356
+ def:pub page() -> JsxElement {
357
+ params = useParams();
358
+ postId = params.id or "";
359
+
360
+ has post: dict = {};
361
+ has isLoading: bool = True;
362
+ has error: str = "";
363
+
364
+ async can with entry {
365
+ result = await fetchPost(postId);
366
+ if result.success { post = result.post; }
367
+ else { error = result.error or "Not found"; }
368
+ isLoading = False;
369
+ }
370
+
371
+ if isLoading { return <p className="text-center py-10 text-gray-400">Loading...</p>; }
372
+ if error {
373
+ return <div className="text-center py-10">
374
+ <p className="text-red-500">{error}</p>
375
+ <Link to="/" className="text-blue-500">Back to posts</Link>
376
+ </div>;
377
+ }
378
+
379
+ return <div>
380
+ <Link to="/" className="text-blue-500 text-sm">Back to posts</Link>
381
+ <h1 className="text-3xl font-bold mt-4">{post["title"] or ""}</h1>
382
+ <div className="mt-5 leading-relaxed whitespace-pre-wrap">{post["body"] or ""}</div>
383
+ <CommentList postId={postId} />
384
+ </div>;
385
+ }
386
+ ```