create-janux 0.1.0 → 0.2.0
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.
- package/package.json +1 -1
- package/template/README.md +34 -0
- package/template/package.json +1 -1
- package/template/src/agent.ts +4 -1
- package/template/src/client.ts +5 -2
- package/template/src/components/Copilot.tsx +103 -0
- package/template/src/components/TaskBoard.test.ts +38 -0
- package/template/src/components/TaskBoard.tsx +125 -0
- package/template/src/components/ThemeToggle.tsx +23 -0
- package/template/src/routes/index.tsx +25 -10
- package/template/src/server/tasks.api.ts +31 -0
- package/template/src/stores.ts +17 -0
- package/template/src/styles.css +236 -124
- package/template/src/components/Counter.tsx +0 -29
- package/template/src/server/hello.api.ts +0 -8
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A [Janux](https://github.com/aralroca/Janux) app. One component, two faces: this task board is a UI for you **and** a set of typed tools for your copilot.
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun run dev # http://localhost:3000
|
|
10
|
+
bun test # the example unit test — no browser needed
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Enable the copilot (optional): copy `.env.example` to `.env` and set `JANUX_MODEL` or one provider API key.
|
|
14
|
+
|
|
15
|
+
## What's inside
|
|
16
|
+
|
|
17
|
+
| File | Shows |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `src/components/TaskBoard.tsx` | State, derived, intents, a `confirm` guard, a debounced persist effect, events |
|
|
20
|
+
| `src/components/ThemeToggle.tsx` | Two islands sharing a store |
|
|
21
|
+
| `src/stores.ts` | Shared state as `store://theme` |
|
|
22
|
+
| `src/server/tasks.api.ts` | api(): endpoint + client stub + agent tool from one definition |
|
|
23
|
+
| `src/components/Copilot.tsx` | The copilot chrome: turn protocol + proposals with human approve |
|
|
24
|
+
| `src/components/TaskBoard.test.ts` | Testing intents & guards without a browser |
|
|
25
|
+
|
|
26
|
+
## See the second face
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -s localhost:3000/_janux/manifest | jq # what agents see
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Try asking the copilot: *"add a task to buy milk, then show me what's left"* — and watch `tasks.clearDone` come back as a proposal you approve on screen.
|
|
33
|
+
|
|
34
|
+
Docs: start with the [tutorial](https://github.com/aralroca/Janux/tree/main/apps/docs/content/tutorial) — it builds exactly this app.
|
package/template/package.json
CHANGED
package/template/src/agent.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { defineAgent } from '@janux/agent';
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
|
-
instructions:
|
|
4
|
+
instructions:
|
|
5
|
+
'You are this task app’s copilot. Use the tasks.* tools to add, toggle, filter and clear ' +
|
|
6
|
+
'tasks, api.tasks.taskStats for stats, and theme.toggle for appearance. clearDone needs ' +
|
|
7
|
+
'human approval — propose it, never insist.',
|
|
5
8
|
});
|
package/template/src/client.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { boot } from 'janux/client';
|
|
2
|
-
import {
|
|
2
|
+
import { TaskBoard } from './components/TaskBoard';
|
|
3
|
+
import { ThemeToggle } from './components/ThemeToggle';
|
|
4
|
+
import { Copilot } from './components/Copilot';
|
|
5
|
+
import { theme } from './stores';
|
|
3
6
|
|
|
4
|
-
boot({ defs: [
|
|
7
|
+
boot({ defs: [TaskBoard, ThemeToggle, Copilot, theme] });
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { component, intent, schema, str, bool, enums, list } from 'janux';
|
|
2
|
+
|
|
3
|
+
let wire: any[] = [];
|
|
4
|
+
|
|
5
|
+
async function postAgent(messages: unknown[], path: string): Promise<any> {
|
|
6
|
+
const response = await fetch('/_janux/agent', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'content-type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ messages, path }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return response.json();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function converse(state: any, path: string): Promise<void> {
|
|
16
|
+
let reply = await postAgent(wire, path);
|
|
17
|
+
|
|
18
|
+
while (reply.type === 'ui_calls') {
|
|
19
|
+
wire = reply.messages;
|
|
20
|
+
for (const call of reply.calls) {
|
|
21
|
+
const result = await (window as any).janux
|
|
22
|
+
.call(call.name, call.input)
|
|
23
|
+
.catch((error: unknown) => ({ error: String(error) }));
|
|
24
|
+
|
|
25
|
+
if (result?.status === 'proposal') state.proposal = { id: result.id, tool: call.name };
|
|
26
|
+
wire.push({ role: 'tool', toolCallId: call.id, content: JSON.stringify(result ?? null) });
|
|
27
|
+
}
|
|
28
|
+
reply = await postAgent(wire, path);
|
|
29
|
+
}
|
|
30
|
+
wire = reply.messages ?? wire;
|
|
31
|
+
state.messages.push({ role: 'assistant', text: reply.type === 'setup' ? reply.message : reply.text });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Copilot = component({
|
|
35
|
+
name: 'copilot',
|
|
36
|
+
description: 'Built-in copilot operating this app through the agent bridge.',
|
|
37
|
+
|
|
38
|
+
state: schema({
|
|
39
|
+
open: bool(),
|
|
40
|
+
busy: bool(),
|
|
41
|
+
messages: list({ role: enums(['user', 'assistant']), text: str() }),
|
|
42
|
+
proposal: schema({ id: str(), tool: str() }).nullable(),
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
intents: {
|
|
46
|
+
toggle: intent({ description: 'Open/close the copilot', run: ({ state }: any) => (state.open = !state.open) }),
|
|
47
|
+
send: intent({
|
|
48
|
+
description: 'Send a message to the copilot',
|
|
49
|
+
input: schema({ text: str().min(1) }),
|
|
50
|
+
run: async ({ state, input }: any) => {
|
|
51
|
+
state.messages.push({ role: 'user', text: input.text });
|
|
52
|
+
state.busy = true;
|
|
53
|
+
wire.push({ role: 'user', content: input.text });
|
|
54
|
+
await converse(state, window.location.pathname).finally(() => (state.busy = false));
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
approve: intent({
|
|
58
|
+
guard: 'forbidden',
|
|
59
|
+
run: async ({ state }: any) => {
|
|
60
|
+
await (window as any).janux.approve(state.proposal.id);
|
|
61
|
+
state.messages.push({ role: 'assistant', text: `Approved ${state.proposal.tool} ✔` });
|
|
62
|
+
state.proposal = null;
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
reject: intent({
|
|
66
|
+
guard: 'forbidden',
|
|
67
|
+
run: ({ state }: any) => {
|
|
68
|
+
(window as any).janux.reject(state.proposal?.id);
|
|
69
|
+
state.proposal = null;
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
view: ({ state, intents }: any) => (
|
|
75
|
+
<aside class="copilot">
|
|
76
|
+
<button class="copilot-toggle" on={intents.toggle}>
|
|
77
|
+
{state.open ? '×' : '✦ Copilot'}
|
|
78
|
+
</button>
|
|
79
|
+
{state.open ? (
|
|
80
|
+
<div class="copilot-panel">
|
|
81
|
+
<ol class="chat">
|
|
82
|
+
{state.messages.map((message: any, index: number) => (
|
|
83
|
+
<li key={String(index)} class={message.role}>
|
|
84
|
+
{message.text}
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ol>
|
|
88
|
+
{state.proposal ? (
|
|
89
|
+
<div class="proposal">
|
|
90
|
+
<p>Run “{state.proposal.tool}”?</p>
|
|
91
|
+
<button on={intents.approve}>Approve</button>
|
|
92
|
+
<button on={intents.reject}>Reject</button>
|
|
93
|
+
</div>
|
|
94
|
+
) : null}
|
|
95
|
+
<form intent={intents.send}>
|
|
96
|
+
<input name="text" placeholder={state.busy ? 'Thinking…' : 'Try: add a task to buy milk'} />
|
|
97
|
+
<button type="submit">Send</button>
|
|
98
|
+
</form>
|
|
99
|
+
</div>
|
|
100
|
+
) : null}
|
|
101
|
+
</aside>
|
|
102
|
+
),
|
|
103
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { createInstance } from 'janux';
|
|
3
|
+
import { TaskBoard } from './TaskBoard';
|
|
4
|
+
|
|
5
|
+
describe('TaskBoard', () => {
|
|
6
|
+
it('adds, toggles and counts tasks', async () => {
|
|
7
|
+
const board = createInstance(TaskBoard);
|
|
8
|
+
|
|
9
|
+
await board.intents.add!({ title: 'Ship it' });
|
|
10
|
+
await board.intents.add!({ title: 'Write tests' });
|
|
11
|
+
const [first] = board.snapshot().tasks as any[];
|
|
12
|
+
|
|
13
|
+
await board.intents.toggle!({ id: first.id });
|
|
14
|
+
expect((board.snapshot().tasks as any[])[0].done).toBe(true);
|
|
15
|
+
expect(board.derived.remaining).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rejects empty titles', () => {
|
|
19
|
+
const board = createInstance(TaskBoard);
|
|
20
|
+
|
|
21
|
+
expect(board.intents.add!({ title: '' })).rejects.toThrow(/below min/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('clearDone is a proposal for agents — nothing happens until approval', async () => {
|
|
25
|
+
let proposal: any;
|
|
26
|
+
const board = createInstance(TaskBoard, { onProposal: (p) => (proposal = p) });
|
|
27
|
+
|
|
28
|
+
await board.intents.add!({ title: 'done thing' });
|
|
29
|
+
await board.intents.toggle!({ id: (board.snapshot().tasks as any[])[0].id });
|
|
30
|
+
|
|
31
|
+
const result: any = await board.intents.clearDone!({}, { origin: 'agent' });
|
|
32
|
+
|
|
33
|
+
expect(result.status).toBe('proposal');
|
|
34
|
+
expect(board.snapshot().tasks).toHaveLength(1);
|
|
35
|
+
await proposal.execute();
|
|
36
|
+
expect(board.snapshot().tasks).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { component, intent, effect, schema, str, bool, int, enums, list } from 'janux';
|
|
2
|
+
import { saveTasks } from '../server/tasks.api';
|
|
3
|
+
import { theme } from '../stores';
|
|
4
|
+
|
|
5
|
+
const FILTERS = ['all', 'active', 'done'] as const;
|
|
6
|
+
|
|
7
|
+
function taskId(): string {
|
|
8
|
+
return `t_${Math.random().toString(36).slice(2, 9)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function visibleTasks(tasks: any[], filter: string): any[] {
|
|
12
|
+
if (filter === 'active') return tasks.filter((task) => !task.done);
|
|
13
|
+
if (filter === 'done') return tasks.filter((task) => task.done);
|
|
14
|
+
|
|
15
|
+
return tasks;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const TaskBoard = component({
|
|
19
|
+
name: 'tasks',
|
|
20
|
+
description: 'A task board. Agents can add, toggle, filter and (with approval) clear done tasks.',
|
|
21
|
+
|
|
22
|
+
state: schema({
|
|
23
|
+
tasks: list({ id: str(), title: str(), done: bool() }),
|
|
24
|
+
filter: enums([...FILTERS]),
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
derived: {
|
|
28
|
+
remaining: (s: any) => s.tasks.filter((task: any) => !task.done).length,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
effects: {
|
|
32
|
+
persist: effect({
|
|
33
|
+
description: 'Saves tasks to the server after changes settle',
|
|
34
|
+
when: (s: any) => s.tasks,
|
|
35
|
+
debounce: '400ms',
|
|
36
|
+
run: ({ state }: any) => saveTasks({ tasks: state.tasks }).then(() => {}),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
emits: { 'tasks.cleared': schema({ count: int() }) },
|
|
41
|
+
|
|
42
|
+
use: { theme },
|
|
43
|
+
|
|
44
|
+
intents: {
|
|
45
|
+
add: intent({
|
|
46
|
+
description: 'Add a task by title',
|
|
47
|
+
input: schema({ title: str().min(1) }),
|
|
48
|
+
run: ({ state, input }: any) => state.tasks.push({ id: taskId(), title: input.title, done: false }),
|
|
49
|
+
}),
|
|
50
|
+
toggle: intent({
|
|
51
|
+
description: 'Toggle a task done/undone by id',
|
|
52
|
+
input: schema({ id: str() }),
|
|
53
|
+
run: ({ state, input }: any) => {
|
|
54
|
+
const task = state.tasks.find((candidate: any) => candidate.id === input.id);
|
|
55
|
+
|
|
56
|
+
if (task) task.done = !task.done;
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
remove: intent({
|
|
60
|
+
description: 'Delete a task by id',
|
|
61
|
+
input: schema({ id: str() }),
|
|
62
|
+
run: ({ state, input }: any) => {
|
|
63
|
+
state.tasks = state.tasks.filter((task: any) => task.id !== input.id);
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
setFilter: intent({
|
|
67
|
+
description: 'Show all, active or done tasks',
|
|
68
|
+
input: schema({ filter: enums([...FILTERS]) }),
|
|
69
|
+
run: ({ state, input }: any) => (state.filter = input.filter),
|
|
70
|
+
}),
|
|
71
|
+
clearDone: intent({
|
|
72
|
+
description: 'Remove every completed task. Destructive — needs approval.',
|
|
73
|
+
guard: 'confirm',
|
|
74
|
+
run: ({ state, emit }: any) => {
|
|
75
|
+
const count = state.tasks.filter((task: any) => task.done).length;
|
|
76
|
+
|
|
77
|
+
state.tasks = state.tasks.filter((task: any) => !task.done);
|
|
78
|
+
emit('tasks.cleared', { count });
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
view: ({ state, derived, intents }: any) => (
|
|
84
|
+
<section class="board">
|
|
85
|
+
<header>
|
|
86
|
+
<h2>Tasks</h2>
|
|
87
|
+
<span class="count">{derived.remaining} left</span>
|
|
88
|
+
</header>
|
|
89
|
+
<form intent={intents.add}>
|
|
90
|
+
<input name="title" placeholder="What needs doing?" autocomplete="off" />
|
|
91
|
+
<button type="submit">Add</button>
|
|
92
|
+
</form>
|
|
93
|
+
<nav class="filters">
|
|
94
|
+
{FILTERS.map((filter) => (
|
|
95
|
+
<button
|
|
96
|
+
key={filter}
|
|
97
|
+
class={state.filter === filter ? 'on' : undefined}
|
|
98
|
+
on={intents.setFilter}
|
|
99
|
+
data-input={JSON.stringify({ filter })}
|
|
100
|
+
>
|
|
101
|
+
{filter}
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</nav>
|
|
105
|
+
<ul class="list">
|
|
106
|
+
{visibleTasks(state.tasks, state.filter).map((task: any) => (
|
|
107
|
+
<li key={task.id} class={task.done ? 'done' : undefined}>
|
|
108
|
+
<button class="check" on={intents.toggle} data-input={JSON.stringify({ id: task.id })}>
|
|
109
|
+
{task.done ? '✓' : ''}
|
|
110
|
+
</button>
|
|
111
|
+
<span>{task.title}</span>
|
|
112
|
+
<button class="x" on={intents.remove} data-input={JSON.stringify({ id: task.id })}>
|
|
113
|
+
✕
|
|
114
|
+
</button>
|
|
115
|
+
</li>
|
|
116
|
+
))}
|
|
117
|
+
</ul>
|
|
118
|
+
<footer>
|
|
119
|
+
<button class="clear" on={intents.clearDone}>
|
|
120
|
+
Clear done
|
|
121
|
+
</button>
|
|
122
|
+
</footer>
|
|
123
|
+
</section>
|
|
124
|
+
),
|
|
125
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { component, intent } from 'janux';
|
|
2
|
+
import { theme } from '../stores';
|
|
3
|
+
|
|
4
|
+
/** Second island sharing the theme store — cross-island state, no props. */
|
|
5
|
+
export const ThemeToggle = component({
|
|
6
|
+
name: 'theme-toggle',
|
|
7
|
+
description: 'Switches the shared theme between dark and light.',
|
|
8
|
+
|
|
9
|
+
use: { theme },
|
|
10
|
+
|
|
11
|
+
intents: {
|
|
12
|
+
toggle: intent({
|
|
13
|
+
description: 'Toggle dark/light mode',
|
|
14
|
+
run: ({ use }: any) => use.theme.intents.toggle({}),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
view: ({ use, intents }: any) => (
|
|
19
|
+
<button class="theme-toggle" on={intents.toggle}>
|
|
20
|
+
{use.theme.state.mode === 'dark' ? '☀️ Light' : '🌙 Dark'}
|
|
21
|
+
</button>
|
|
22
|
+
),
|
|
23
|
+
});
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadTasks } from '../server/tasks.api';
|
|
2
|
+
import { TaskBoard } from '../components/TaskBoard';
|
|
3
|
+
import { ThemeToggle } from '../components/ThemeToggle';
|
|
4
|
+
import { Copilot } from '../components/Copilot';
|
|
5
|
+
|
|
6
|
+
export const meta = {
|
|
7
|
+
title: 'Tasks — a Janux app',
|
|
8
|
+
description: 'A task board with two faces: a UI for you, typed tools for your copilot.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default async function Home() {
|
|
12
|
+
const saved: any = await loadTasks({});
|
|
2
13
|
|
|
3
|
-
export default function Home() {
|
|
4
14
|
return (
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
<div class="app">
|
|
16
|
+
<header class="topbar">
|
|
17
|
+
<span class="brand">✦ Tasks</span>
|
|
18
|
+
<ThemeToggle />
|
|
19
|
+
</header>
|
|
20
|
+
<main>
|
|
21
|
+
<TaskBoard initial={{ tasks: saved.tasks, filter: 'all' }} />
|
|
22
|
+
<p class="hint">
|
|
23
|
+
This board is also an agent surface — <code>curl localhost:3000/_janux/manifest</code>
|
|
24
|
+
</p>
|
|
25
|
+
</main>
|
|
26
|
+
<Copilot persist />
|
|
27
|
+
</div>
|
|
13
28
|
);
|
|
14
29
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { api } from '@janux/server';
|
|
2
|
+
import { schema, str, bool, int, list } from 'janux';
|
|
3
|
+
|
|
4
|
+
const TASK_SHAPE = { id: str(), title: str(), done: bool() };
|
|
5
|
+
const saved = new Map<string, unknown[]>();
|
|
6
|
+
|
|
7
|
+
export const loadTasks = api({
|
|
8
|
+
description: 'Load the saved task list',
|
|
9
|
+
output: schema({ tasks: list(TASK_SHAPE) }),
|
|
10
|
+
run: ({ ctx }) => ({ tasks: (saved.get(String(ctx.userId ?? 'anon')) as any[]) ?? [] }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const saveTasks = api({
|
|
14
|
+
description: 'Persist the task list',
|
|
15
|
+
input: schema({ tasks: list(TASK_SHAPE) }),
|
|
16
|
+
run: ({ input, ctx }) => {
|
|
17
|
+
saved.set(String(ctx.userId ?? 'anon'), input.tasks);
|
|
18
|
+
|
|
19
|
+
return { saved: input.tasks.length };
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const taskStats = api({
|
|
24
|
+
description: 'Aggregate stats over the saved tasks',
|
|
25
|
+
output: schema({ total: int(), done: int() }),
|
|
26
|
+
run: ({ ctx }) => {
|
|
27
|
+
const tasks = (saved.get(String(ctx.userId ?? 'anon')) as any[]) ?? [];
|
|
28
|
+
|
|
29
|
+
return { total: tasks.length, done: tasks.filter((task) => task.done).length };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { store, intent, schema, enums } from 'janux';
|
|
2
|
+
|
|
3
|
+
/** Shared theme, projected to agents as store://theme. */
|
|
4
|
+
export const theme = store({
|
|
5
|
+
name: 'theme',
|
|
6
|
+
description: 'UI theme shared by every island.',
|
|
7
|
+
state: schema({ mode: enums(['dark', 'light']).default('dark') }),
|
|
8
|
+
intents: {
|
|
9
|
+
toggle: intent({
|
|
10
|
+
description: 'Switch between dark and light mode',
|
|
11
|
+
run: ({ state }) => {
|
|
12
|
+
state.mode = state.mode === 'dark' ? 'light' : 'dark';
|
|
13
|
+
if (typeof document !== 'undefined') document.documentElement.dataset.theme = state.mode;
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
});
|
package/template/src/styles.css
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--
|
|
2
|
+
--bg: #0e0d24;
|
|
3
|
+
--card: #1b1938;
|
|
4
|
+
--card-2: #242150;
|
|
5
|
+
--text: #e2e8f0;
|
|
6
|
+
--muted: #94a3b8;
|
|
7
|
+
--violet: #a78bfa;
|
|
8
|
+
--cyan: #22d3ee;
|
|
9
|
+
--border: #312e81;
|
|
10
|
+
--danger: #f87171;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:root[data-theme='light'] {
|
|
14
|
+
--bg: #f8fafc;
|
|
15
|
+
--card: #ffffff;
|
|
16
|
+
--card-2: #eef2ff;
|
|
17
|
+
--text: #0f172a;
|
|
18
|
+
--muted: #64748b;
|
|
3
19
|
--violet: #7c3aed;
|
|
4
|
-
--cyan: #
|
|
5
|
-
--text: #334155;
|
|
20
|
+
--cyan: #0891b2;
|
|
6
21
|
--border: #e2e8f0;
|
|
7
|
-
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
8
22
|
}
|
|
9
23
|
|
|
10
24
|
* {
|
|
@@ -13,208 +27,306 @@
|
|
|
13
27
|
|
|
14
28
|
body {
|
|
15
29
|
margin: 0;
|
|
16
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter,
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
|
|
31
|
+
background: var(--bg);
|
|
17
32
|
color: var(--text);
|
|
18
|
-
|
|
19
|
-
-webkit-font-smoothing: antialiased;
|
|
33
|
+
transition: background 0.25s, color 0.25s;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
.topbar {
|
|
37
|
+
display: flex;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
align-items: center;
|
|
40
|
+
max-width: 560px;
|
|
24
41
|
margin: 0 auto;
|
|
25
|
-
padding:
|
|
42
|
+
padding: 22px 16px 0;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
.brand {
|
|
46
|
+
font-weight: 800;
|
|
47
|
+
font-size: 20px;
|
|
48
|
+
background: linear-gradient(90deg, var(--violet), var(--cyan));
|
|
49
|
+
-webkit-background-clip: text;
|
|
50
|
+
background-clip: text;
|
|
51
|
+
color: transparent;
|
|
32
52
|
}
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
.theme-toggle {
|
|
55
|
+
padding: 7px 14px;
|
|
56
|
+
border: 1px solid var(--border);
|
|
57
|
+
border-radius: 999px;
|
|
58
|
+
background: var(--card);
|
|
59
|
+
color: var(--text);
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
font-size: 13px;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
letter-spacing: -0.03em;
|
|
43
|
-
color: var(--ink);
|
|
64
|
+
main {
|
|
65
|
+
max-width: 560px;
|
|
66
|
+
margin: 24px auto 120px;
|
|
67
|
+
padding: 0 16px;
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
color: var(--
|
|
70
|
+
.hint {
|
|
71
|
+
text-align: center;
|
|
72
|
+
color: var(--muted);
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.hint code {
|
|
77
|
+
background: var(--card-2);
|
|
78
|
+
padding: 2px 8px;
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
font-size: 12px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── Board ──────────────────────────────────────────────────────── */
|
|
84
|
+
.board {
|
|
85
|
+
background: var(--card);
|
|
86
|
+
border: 1px solid var(--border);
|
|
87
|
+
border-radius: 18px;
|
|
88
|
+
padding: 22px;
|
|
89
|
+
box-shadow: 0 20px 50px -30px rgba(0, 0, 0, 0.6);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.board header {
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
align-items: baseline;
|
|
96
|
+
margin-bottom: 14px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.board h2 {
|
|
100
|
+
margin: 0;
|
|
101
|
+
font-size: 1.3rem;
|
|
49
102
|
letter-spacing: -0.02em;
|
|
50
103
|
}
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
color: var(--
|
|
54
|
-
font-
|
|
105
|
+
.board .count {
|
|
106
|
+
color: var(--muted);
|
|
107
|
+
font-size: 13px;
|
|
55
108
|
}
|
|
56
109
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
.board form {
|
|
111
|
+
display: flex;
|
|
112
|
+
gap: 8px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.board input {
|
|
116
|
+
flex: 1;
|
|
117
|
+
padding: 11px 14px;
|
|
118
|
+
border: 1px solid var(--border);
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
background: var(--bg);
|
|
121
|
+
color: var(--text);
|
|
122
|
+
font-size: 14.5px;
|
|
123
|
+
outline: none;
|
|
64
124
|
}
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
126
|
+
.board input:focus {
|
|
127
|
+
border-color: var(--violet);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.board button {
|
|
68
131
|
border: none;
|
|
69
|
-
border-radius:
|
|
70
|
-
background: var(--ink);
|
|
71
|
-
color: #fff;
|
|
72
|
-
font-size: 14px;
|
|
73
|
-
font-weight: 600;
|
|
132
|
+
border-radius: 10px;
|
|
74
133
|
cursor: pointer;
|
|
75
|
-
|
|
134
|
+
font-weight: 600;
|
|
76
135
|
}
|
|
77
136
|
|
|
78
|
-
button
|
|
79
|
-
|
|
80
|
-
|
|
137
|
+
.board form button {
|
|
138
|
+
padding: 0 18px;
|
|
139
|
+
background: linear-gradient(90deg, #7c3aed, #06b6d4);
|
|
140
|
+
color: #fff;
|
|
81
141
|
}
|
|
82
142
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
border-radius: 16px;
|
|
88
|
-
background: #fff;
|
|
89
|
-
box-shadow: 0 8px 30px -18px rgba(30, 27, 75, 0.35);
|
|
143
|
+
.filters {
|
|
144
|
+
display: flex;
|
|
145
|
+
gap: 6px;
|
|
146
|
+
margin: 14px 0 6px;
|
|
90
147
|
}
|
|
91
148
|
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
149
|
+
.filters button {
|
|
150
|
+
padding: 5px 14px;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
background: transparent;
|
|
153
|
+
color: var(--muted);
|
|
154
|
+
border: 1px solid transparent;
|
|
155
|
+
font-size: 13px;
|
|
156
|
+
text-transform: capitalize;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.filters button.on {
|
|
160
|
+
background: var(--card-2);
|
|
161
|
+
color: var(--text);
|
|
162
|
+
border-color: var(--border);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.list {
|
|
95
166
|
list-style: none;
|
|
167
|
+
margin: 8px 0;
|
|
168
|
+
padding: 0;
|
|
96
169
|
}
|
|
97
170
|
|
|
98
|
-
.
|
|
99
|
-
.cart .items li {
|
|
171
|
+
.list li {
|
|
100
172
|
display: flex;
|
|
101
173
|
align-items: center;
|
|
102
|
-
justify-content: space-between;
|
|
103
174
|
gap: 12px;
|
|
104
175
|
padding: 10px 4px;
|
|
105
176
|
border-bottom: 1px solid var(--border);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.list li span {
|
|
180
|
+
flex: 1;
|
|
106
181
|
font-size: 15px;
|
|
107
182
|
}
|
|
108
183
|
|
|
109
|
-
.
|
|
110
|
-
|
|
184
|
+
.list li.done span {
|
|
185
|
+
text-decoration: line-through;
|
|
186
|
+
color: var(--muted);
|
|
111
187
|
}
|
|
112
188
|
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
189
|
+
.check {
|
|
190
|
+
width: 26px;
|
|
191
|
+
height: 26px;
|
|
192
|
+
border-radius: 50%;
|
|
193
|
+
border: 2px solid var(--violet) !important;
|
|
194
|
+
background: transparent;
|
|
195
|
+
color: var(--cyan);
|
|
196
|
+
font-size: 14px;
|
|
197
|
+
line-height: 1;
|
|
116
198
|
}
|
|
117
199
|
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
font-weight: 800;
|
|
122
|
-
color: var(--ink);
|
|
200
|
+
li.done .check {
|
|
201
|
+
background: var(--violet);
|
|
202
|
+
color: #fff;
|
|
123
203
|
}
|
|
124
204
|
|
|
125
|
-
.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
205
|
+
.x {
|
|
206
|
+
background: transparent;
|
|
207
|
+
color: var(--muted);
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
padding: 4px 8px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.x:hover {
|
|
213
|
+
color: var(--danger);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.board footer {
|
|
217
|
+
display: flex;
|
|
218
|
+
justify-content: flex-end;
|
|
219
|
+
margin-top: 10px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.clear {
|
|
223
|
+
background: transparent;
|
|
224
|
+
color: var(--muted);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
padding: 6px 10px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.clear:hover {
|
|
230
|
+
color: var(--danger);
|
|
131
231
|
}
|
|
132
232
|
|
|
133
233
|
/* ── Copilot ────────────────────────────────────────────────────── */
|
|
134
|
-
.copilot {
|
|
234
|
+
.copilot-toggle {
|
|
235
|
+
position: fixed;
|
|
236
|
+
right: 22px;
|
|
237
|
+
bottom: 22px;
|
|
238
|
+
z-index: 50;
|
|
239
|
+
padding: 12px 20px;
|
|
240
|
+
border: none;
|
|
241
|
+
border-radius: 999px;
|
|
242
|
+
background: linear-gradient(90deg, #7c3aed, #06b6d4);
|
|
243
|
+
color: #fff;
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
box-shadow: 0 10px 30px -8px rgba(124, 58, 237, 0.6);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.copilot-panel {
|
|
250
|
+
position: fixed;
|
|
251
|
+
right: 22px;
|
|
252
|
+
bottom: 78px;
|
|
253
|
+
z-index: 50;
|
|
135
254
|
display: flex;
|
|
136
255
|
flex-direction: column;
|
|
137
|
-
|
|
138
|
-
|
|
256
|
+
width: 340px;
|
|
257
|
+
max-height: 480px;
|
|
139
258
|
border: 1px solid var(--border);
|
|
140
259
|
border-radius: 16px;
|
|
141
|
-
background:
|
|
142
|
-
box-shadow: 0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.copilot h3 {
|
|
146
|
-
margin: 0 0 12px;
|
|
260
|
+
background: var(--card);
|
|
261
|
+
box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.5);
|
|
262
|
+
overflow: hidden;
|
|
147
263
|
}
|
|
148
264
|
|
|
149
|
-
.copilot .chat {
|
|
150
|
-
|
|
151
|
-
|
|
265
|
+
.copilot-panel .chat {
|
|
266
|
+
flex: 1;
|
|
267
|
+
margin: 0;
|
|
268
|
+
padding: 14px;
|
|
152
269
|
list-style: none;
|
|
153
|
-
max-height: 340px;
|
|
154
270
|
overflow-y: auto;
|
|
271
|
+
min-height: 100px;
|
|
155
272
|
}
|
|
156
273
|
|
|
157
|
-
.copilot .chat li {
|
|
274
|
+
.copilot-panel .chat li {
|
|
158
275
|
margin-bottom: 8px;
|
|
159
276
|
padding: 9px 13px;
|
|
160
277
|
border-radius: 12px;
|
|
161
|
-
font-size:
|
|
162
|
-
line-height: 1.5;
|
|
278
|
+
font-size: 13.5px;
|
|
163
279
|
max-width: 88%;
|
|
164
280
|
}
|
|
165
281
|
|
|
166
|
-
.copilot .chat li.user {
|
|
282
|
+
.copilot-panel .chat li.user {
|
|
167
283
|
margin-left: auto;
|
|
168
|
-
background:
|
|
284
|
+
background: #6d28d9;
|
|
169
285
|
color: #fff;
|
|
170
286
|
}
|
|
171
287
|
|
|
172
|
-
.copilot .chat li.assistant {
|
|
173
|
-
background:
|
|
174
|
-
color: var(--ink);
|
|
288
|
+
.copilot-panel .chat li.assistant {
|
|
289
|
+
background: var(--card-2);
|
|
175
290
|
}
|
|
176
291
|
|
|
177
|
-
.copilot form {
|
|
292
|
+
.copilot-panel form {
|
|
178
293
|
display: flex;
|
|
179
294
|
gap: 8px;
|
|
295
|
+
padding: 10px;
|
|
296
|
+
border-top: 1px solid var(--border);
|
|
180
297
|
}
|
|
181
298
|
|
|
182
|
-
.copilot input {
|
|
299
|
+
.copilot-panel input {
|
|
183
300
|
flex: 1;
|
|
184
|
-
padding: 9px
|
|
301
|
+
padding: 9px 12px;
|
|
185
302
|
border: 1px solid var(--border);
|
|
186
303
|
border-radius: 9px;
|
|
187
|
-
|
|
304
|
+
background: var(--bg);
|
|
305
|
+
color: var(--text);
|
|
306
|
+
font-size: 13.5px;
|
|
188
307
|
outline: none;
|
|
189
308
|
}
|
|
190
309
|
|
|
191
|
-
.copilot
|
|
192
|
-
border
|
|
193
|
-
|
|
310
|
+
.copilot-panel button {
|
|
311
|
+
border: none;
|
|
312
|
+
border-radius: 9px;
|
|
313
|
+
padding: 9px 14px;
|
|
314
|
+
background: var(--violet);
|
|
315
|
+
color: #fff;
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
cursor: pointer;
|
|
194
318
|
}
|
|
195
319
|
|
|
196
320
|
.proposal {
|
|
197
|
-
margin: 0
|
|
198
|
-
padding:
|
|
199
|
-
border: 1px solid #
|
|
200
|
-
border-radius:
|
|
201
|
-
background:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.proposal p {
|
|
205
|
-
margin: 0 0 10px;
|
|
206
|
-
font-size: 14px;
|
|
207
|
-
color: #92400e;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.proposal button + button {
|
|
211
|
-
margin-left: 8px;
|
|
212
|
-
background: #e2e8f0;
|
|
213
|
-
color: var(--ink);
|
|
321
|
+
margin: 0 14px 10px;
|
|
322
|
+
padding: 10px 12px;
|
|
323
|
+
border: 1px solid #fbbf24;
|
|
324
|
+
border-radius: 10px;
|
|
325
|
+
background: rgba(251, 191, 36, 0.1);
|
|
326
|
+
font-size: 13px;
|
|
214
327
|
}
|
|
215
328
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
329
|
+
.proposal button {
|
|
330
|
+
margin-right: 6px;
|
|
331
|
+
margin-top: 6px;
|
|
220
332
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { component, intent, schema, int } from 'janux';
|
|
2
|
-
|
|
3
|
-
export const Counter = component({
|
|
4
|
-
name: 'counter',
|
|
5
|
-
description: 'A counter. Agents can read it (ui://counter) and bump it (counter.inc).',
|
|
6
|
-
|
|
7
|
-
state: schema({ n: int() }),
|
|
8
|
-
|
|
9
|
-
intents: {
|
|
10
|
-
inc: intent({
|
|
11
|
-
description: 'Increment the counter',
|
|
12
|
-
input: schema({ by: int().default(1) }),
|
|
13
|
-
run: ({ state, input }) => (state.n += input.by),
|
|
14
|
-
}),
|
|
15
|
-
reset: intent({
|
|
16
|
-
description: 'Reset the counter to zero',
|
|
17
|
-
guard: 'confirm',
|
|
18
|
-
run: ({ state }) => (state.n = 0),
|
|
19
|
-
}),
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
view: ({ state, intents }) => (
|
|
23
|
-
<section>
|
|
24
|
-
<output>{state.n}</output>
|
|
25
|
-
<button on={intents.inc}>+1</button>
|
|
26
|
-
<button on={intents.reset}>Reset</button>
|
|
27
|
-
</section>
|
|
28
|
-
),
|
|
29
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { api } from '@janux/server';
|
|
2
|
-
import { schema, str } from 'janux';
|
|
3
|
-
|
|
4
|
-
export const greet = api({
|
|
5
|
-
description: 'Greet someone by name',
|
|
6
|
-
input: schema({ name: str() }),
|
|
7
|
-
run: ({ input }) => ({ message: `Hello, ${input.name}! — from a Janux api() tool` }),
|
|
8
|
-
});
|