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.
- jac_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|
+
```
|