ltcai 5.6.0 → 6.0.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/README.md +42 -25
- package/docs/CHANGELOG.md +38 -0
- package/frontend/openapi.json +39 -0
- package/frontend/src/api/client.ts +104 -23
- package/frontend/src/api/openapi.ts +48 -0
- package/frontend/src/components/FirstRunGuide.tsx +3 -3
- package/frontend/src/features/review/ReviewCard.tsx +91 -0
- package/frontend/src/features/review/ReviewInbox.tsx +112 -0
- package/frontend/src/features/review/reviewHelpers.ts +69 -0
- package/frontend/src/i18n.ts +8 -8
- package/frontend/src/pages/Act.tsx +5 -177
- package/frontend/src/routes.ts +1 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/review_queue.py +7 -3
- package/latticeai/app_factory.py +224 -473
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/app_context_runtime.py +13 -0
- package/latticeai/runtime/automation_runtime.py +64 -0
- package/latticeai/runtime/bootstrap.py +48 -0
- package/latticeai/runtime/context_runtime.py +43 -0
- package/latticeai/runtime/hooks_runtime.py +77 -0
- package/latticeai/runtime/lifespan_runtime.py +138 -0
- package/latticeai/runtime/persistence_runtime.py +87 -0
- package/latticeai/runtime/platform_services_runtime.py +39 -0
- package/latticeai/runtime/router_registration.py +570 -0
- package/latticeai/runtime/web_runtime.py +65 -0
- package/latticeai/services/review_queue.py +20 -4
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +3 -3
- package/static/app/assets/index-D2zafMYb.js +16 -0
- package/static/app/assets/index-D2zafMYb.js.map +1 -0
- package/static/app/index.html +1 -1
- package/static/app/assets/index-xMFu94cX.js +0 -16
- package/static/app/assets/index-xMFu94cX.js.map +0 -1
package/README.md
CHANGED
|
@@ -69,35 +69,42 @@ You need Lattice AI when:
|
|
|
69
69
|
Choose the owner of the Brain. The profile is not a SaaS account by default; it
|
|
70
70
|
is the local identity for the knowledge you keep.
|
|
71
71
|
|
|
72
|
-

|
|
73
73
|
|
|
74
74
|
### 2. Environment Analysis
|
|
75
75
|
|
|
76
76
|
See what kind of local AI experience this computer can support before choosing a
|
|
77
77
|
model.
|
|
78
78
|
|
|
79
|
-

|
|
80
80
|
|
|
81
81
|
### 3. Recommended Models
|
|
82
82
|
|
|
83
83
|
Start with a short list: safest recommendation, faster model, stronger model.
|
|
84
84
|
Advanced details stay available without overwhelming first-time users.
|
|
85
85
|
|
|
86
|
-

|
|
87
87
|
|
|
88
88
|
### 4. Install And Load
|
|
89
89
|
|
|
90
90
|
Download and load only after consent. Lattice explains model size, local
|
|
91
91
|
execution, and network use before work starts.
|
|
92
92
|
|
|
93
|
-

|
|
94
94
|
|
|
95
95
|
### 5. Brain Chat
|
|
96
96
|
|
|
97
97
|
Talk normally. Useful decisions and context become memory, then appear later as
|
|
98
98
|
topics, relationships, and graph structure.
|
|
99
99
|
|
|
100
|
-

|
|
101
|
+
|
|
102
|
+
### 6. Review Center
|
|
103
|
+
|
|
104
|
+
Automation results are staged for review before they become durable decisions.
|
|
105
|
+
Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
|
|
106
|
+
|
|
107
|
+

|
|
101
108
|
|
|
102
109
|
## Brain Depths
|
|
103
110
|
|
|
@@ -113,10 +120,10 @@ The user travels inward from everyday memory to deeper structure:
|
|
|
113
120
|
|
|
114
121
|
Walkthrough:
|
|
115
122
|
|
|
116
|
-

|
|
117
124
|
|
|
118
125
|
Screenshot index and capture notes:
|
|
119
|
-
[output/release/
|
|
126
|
+
[output/release/v6.0.0/SCREENSHOT_INDEX.md](output/release/v6.0.0/SCREENSHOT_INDEX.md)
|
|
120
127
|
|
|
121
128
|
## Install
|
|
122
129
|
|
|
@@ -193,24 +200,33 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
|
|
|
193
200
|
|
|
194
201
|
## Current Release Preparation
|
|
195
202
|
|
|
196
|
-
The current development target is **
|
|
197
|
-
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
- `/automation/reviews`
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
The current development target is **6.0.0 Product Reset / Review Center Completion**:
|
|
204
|
+
|
|
205
|
+
- Review Center now includes Pending, Snoozed, and All filters so users can find
|
|
206
|
+
deferred automation suggestions without waiting for expiry.
|
|
207
|
+
- `/automation/reviews` adds explicit `unsnooze` support while preserving
|
|
208
|
+
read-time snooze expiry and `run_now != approve` semantics.
|
|
209
|
+
- Review Center frontend code is split into `frontend/src/features/review/`
|
|
210
|
+
modules instead of living inside the Act page.
|
|
211
|
+
- Review item frontend types are derived from regenerated OpenAPI component
|
|
212
|
+
- schemas, and Review Center API calls use generated operation paths for list
|
|
213
|
+
and state-transition actions.
|
|
214
|
+
- `app_factory.py` is decomposed behind runtime seams for session, hooks, web
|
|
215
|
+
shell, persistence, lifespan, automation, context/search, platform services,
|
|
216
|
+
app context, and router registration while preserving the frozen 364-entry
|
|
217
|
+
route/mount snapshot.
|
|
218
|
+
- First-run copy now states the local-first trust boundary more directly:
|
|
219
|
+
knowledge stays on this computer by default, downloads/external transfer start
|
|
220
|
+
only after user action, and models remain replaceable.
|
|
221
|
+
- All package/runtime/static/OpenAPI versions are synchronized to 6.0.0.
|
|
222
|
+
|
|
223
|
+
Expected artifacts for 6.0.0 release must use exact filenames:
|
|
224
|
+
|
|
225
|
+
- `dist/ltcai-6.0.0-py3-none-any.whl`
|
|
226
|
+
- `dist/ltcai-6.0.0.tar.gz`
|
|
227
|
+
- `ltcai-6.0.0.tgz`
|
|
228
|
+
- `dist/ltcai-6.0.0.vsix`
|
|
229
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_6.0.0_aarch64.dmg`
|
|
214
230
|
|
|
215
231
|
Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
216
232
|
|
|
@@ -229,6 +245,7 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
|
229
245
|
|
|
230
246
|
| Version | Theme |
|
|
231
247
|
| --- | --- |
|
|
248
|
+
| 6.0.0 | Product Reset / Review Center Completion: Snoozed filter, Unsnooze, OpenAPI-derived Review typing, Review feature extraction, v6 docs and scorecard |
|
|
232
249
|
| 5.6.0 | Brain Automation Review Center: workspace-scoped automation review inbox, source-aware provenance, guarded approve/dismiss/snooze/run_now actions, and Act Review tab |
|
|
233
250
|
| 5.5.0 | Release Coordination: synchronized package/runtime/static versions and release docs for the 5.5.0 line while preserving v5.4.0 Brain Automation Scheduler behavior |
|
|
234
251
|
| 5.4.0 | Brain Automation Scheduler: consent-first recipe drafts (Daily/Weekly/Follow-up), TriggerService with dedup/LATTICE_TZ/degraded, runtime graph cleanup, E2E scenarios |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,44 @@
|
|
|
3
3
|
The top entry is the current release-preparation target. Older entries are
|
|
4
4
|
historical and may describe behavior as it existed at that release.
|
|
5
5
|
|
|
6
|
+
## [6.0.0] - 2026-06-15
|
|
7
|
+
|
|
8
|
+
> Product Reset / Review Center Completion. Raises the Review Center from a
|
|
9
|
+
> pending-only inbox into a reversible automation review surface while
|
|
10
|
+
> documenting the v6 quality uplift honestly.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Review Center status filters for Pending, Snoozed, and All.
|
|
14
|
+
- Explicit `POST /automation/reviews/{item_id}/unsnooze` API and backend policy.
|
|
15
|
+
- Frontend Unsnooze action and clear `snoozed_until` presentation.
|
|
16
|
+
- `docs/v6/PLAN.md`, `ARCHITECTURE_REVIEW.md`, `UX_REVIEW.md`, and
|
|
17
|
+
`QUALITY_SCORECARD.md`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Review Center frontend moved from `Act.tsx` into
|
|
21
|
+
`frontend/src/features/review/` components and helpers.
|
|
22
|
+
- Review item frontend types now alias generated OpenAPI component schemas.
|
|
23
|
+
- Review Center API calls now use generated OpenAPI operation paths for list,
|
|
24
|
+
approve, dismiss, snooze, unsnooze, and run_now actions.
|
|
25
|
+
- `app_factory.py` runtime assembly moved behind session, hooks, web,
|
|
26
|
+
persistence, lifespan, automation, context/search, platform service, app
|
|
27
|
+
context, and router-registration seams while preserving the frozen 364-entry
|
|
28
|
+
route/mount snapshot.
|
|
29
|
+
- First-run/onboarding copy now states local-first trust boundaries more
|
|
30
|
+
directly: local knowledge by default, explicit downloads, and explicit
|
|
31
|
+
external transfer.
|
|
32
|
+
- README release evidence screenshots and walkthrough GIF are refreshed under
|
|
33
|
+
`output/release/v6.0.0/`, including the Review Center surface.
|
|
34
|
+
- OpenAPI artifacts and synchronized package/runtime/static metadata now target
|
|
35
|
+
`6.0.0`.
|
|
36
|
+
|
|
37
|
+
### Preserved
|
|
38
|
+
- `run_now` remains preview/regenerate and does not approve.
|
|
39
|
+
- Snooze expiry remains read-time only; explicit unsnooze is the only immediate
|
|
40
|
+
return-to-pending mutation.
|
|
41
|
+
- Package publishing, GitHub Release creation, artifact upload, and merge to
|
|
42
|
+
`main` remain out of scope for this branch.
|
|
43
|
+
|
|
6
44
|
## [5.6.0] - 2026-06-15
|
|
7
45
|
|
|
8
46
|
> Brain Automation Review Center. Adds a workspace-scoped review inbox for
|
package/frontend/openapi.json
CHANGED
|
@@ -7867,6 +7867,45 @@
|
|
|
7867
7867
|
"summary": "Snooze Item"
|
|
7868
7868
|
}
|
|
7869
7869
|
},
|
|
7870
|
+
"/automation/reviews/{item_id}/unsnooze": {
|
|
7871
|
+
"post": {
|
|
7872
|
+
"operationId": "unsnooze_item_automation_reviews__item_id__unsnooze_post",
|
|
7873
|
+
"parameters": [
|
|
7874
|
+
{
|
|
7875
|
+
"in": "path",
|
|
7876
|
+
"name": "item_id",
|
|
7877
|
+
"required": true,
|
|
7878
|
+
"schema": {
|
|
7879
|
+
"title": "Item Id",
|
|
7880
|
+
"type": "string"
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
7883
|
+
],
|
|
7884
|
+
"responses": {
|
|
7885
|
+
"200": {
|
|
7886
|
+
"content": {
|
|
7887
|
+
"application/json": {
|
|
7888
|
+
"schema": {
|
|
7889
|
+
"$ref": "#/components/schemas/ReviewItem"
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
},
|
|
7893
|
+
"description": "Successful Response"
|
|
7894
|
+
},
|
|
7895
|
+
"422": {
|
|
7896
|
+
"content": {
|
|
7897
|
+
"application/json": {
|
|
7898
|
+
"schema": {
|
|
7899
|
+
"$ref": "#/components/schemas/HTTPValidationError"
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
},
|
|
7903
|
+
"description": "Validation Error"
|
|
7904
|
+
}
|
|
7905
|
+
},
|
|
7906
|
+
"summary": "Unsnooze Item"
|
|
7907
|
+
}
|
|
7908
|
+
},
|
|
7870
7909
|
"/chat": {
|
|
7871
7910
|
"get": {
|
|
7872
7911
|
"operationId": "chat_page_chat_get",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createClient from "openapi-fetch";
|
|
2
|
-
import type { paths } from "./openapi";
|
|
2
|
+
import type { components, operations, paths } from "./openapi";
|
|
3
3
|
import { useAppStore } from "@/store/appStore";
|
|
4
4
|
|
|
5
5
|
export type ApiResult<T = unknown> = {
|
|
@@ -21,7 +21,19 @@ export type AdminAuditFilters = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const TIMEOUT_MS = 10_000;
|
|
24
|
-
|
|
24
|
+
type OpenApiClient = ReturnType<typeof createClient<paths>>;
|
|
25
|
+
type OperationJson200<Operation> = Operation extends {
|
|
26
|
+
responses: { 200: { content: { "application/json": infer Result } } };
|
|
27
|
+
} ? Result : never;
|
|
28
|
+
type ReviewListOperation = operations["list_items_automation_reviews_get"];
|
|
29
|
+
type ReviewItemOperation =
|
|
30
|
+
| operations["approve_item_automation_reviews__item_id__approve_post"]
|
|
31
|
+
| operations["dismiss_item_automation_reviews__item_id__dismiss_post"]
|
|
32
|
+
| operations["run_now_item_automation_reviews__item_id__run_now_post"]
|
|
33
|
+
| operations["snooze_item_automation_reviews__item_id__snooze_post"]
|
|
34
|
+
| operations["unsnooze_item_automation_reviews__item_id__unsnooze_post"];
|
|
35
|
+
|
|
36
|
+
const clients = new Map<string, OpenApiClient>();
|
|
25
37
|
let desktopBase: Promise<string | null> | null = null;
|
|
26
38
|
|
|
27
39
|
declare global {
|
|
@@ -145,6 +157,39 @@ async function apiJson<T>(
|
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
async function openApiJson<T>(
|
|
161
|
+
shape: T,
|
|
162
|
+
execute: (client: OpenApiClient, signal: AbortSignal) => Promise<{ data?: T; error?: unknown; response: Response }>,
|
|
163
|
+
): Promise<ApiResult<T>> {
|
|
164
|
+
const base = await apiBase();
|
|
165
|
+
const client = clientFor(base);
|
|
166
|
+
const ctrl = new AbortController();
|
|
167
|
+
const timer = window.setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
168
|
+
try {
|
|
169
|
+
const { data, error, response } = await execute(client, ctrl.signal);
|
|
170
|
+
if (response.ok && data !== undefined) {
|
|
171
|
+
return { ok: true, status: response.status, data, source: "live" };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
status: response.status,
|
|
176
|
+
data: emptyFor(shape),
|
|
177
|
+
source: "unavailable",
|
|
178
|
+
error: friendlyError(error, response.statusText),
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
status: 0,
|
|
184
|
+
data: emptyFor(shape),
|
|
185
|
+
source: "unavailable",
|
|
186
|
+
error: err instanceof Error ? err.message : String(err),
|
|
187
|
+
};
|
|
188
|
+
} finally {
|
|
189
|
+
window.clearTimeout(timer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
148
193
|
function get<T>(path: string, shape: T, query?: Query) {
|
|
149
194
|
return apiJson<T>("GET", path, { query, shape });
|
|
150
195
|
}
|
|
@@ -161,20 +206,10 @@ function del<T>(path: string, shape: T) {
|
|
|
161
206
|
return apiJson<T>("DELETE", path, { shape });
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
export type ReviewItem =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
title: string;
|
|
169
|
-
summary?: string;
|
|
170
|
-
source?: string;
|
|
171
|
-
kind?: string;
|
|
172
|
-
payload?: Record<string, unknown>;
|
|
173
|
-
provenance?: Record<string, unknown>;
|
|
174
|
-
snoozed_until?: string | null;
|
|
175
|
-
created_at?: string | null;
|
|
176
|
-
updated_at?: string | null;
|
|
177
|
-
};
|
|
209
|
+
export type ReviewItem = components["schemas"]["ReviewItem"];
|
|
210
|
+
export type ReviewItemList = components["schemas"]["ReviewItemList"];
|
|
211
|
+
export type ReviewStatusFilter = "pending" | "snoozed" | "approved" | "dismissed" | "all";
|
|
212
|
+
export type ReviewSourceFilter = "workflow_run" | "trigger" | "kg_change_digest" | "all";
|
|
178
213
|
|
|
179
214
|
function reviewItemShape(): ReviewItem {
|
|
180
215
|
return {
|
|
@@ -190,6 +225,53 @@ function reviewItemShape(): ReviewItem {
|
|
|
190
225
|
};
|
|
191
226
|
}
|
|
192
227
|
|
|
228
|
+
function reviewItemListShape(): ReviewItemList {
|
|
229
|
+
return { items: [] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reviewList(query?: { status?: Exclude<ReviewStatusFilter, "all">; source?: Exclude<ReviewSourceFilter, "all"> }) {
|
|
233
|
+
return openApiJson<OperationJson200<ReviewListOperation>>(
|
|
234
|
+
reviewItemListShape(),
|
|
235
|
+
(client, signal) => client.GET("/automation/reviews", {
|
|
236
|
+
params: { query: query || {} },
|
|
237
|
+
headers: workspaceHeaders(),
|
|
238
|
+
signal,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function reviewAction(
|
|
244
|
+
id: string,
|
|
245
|
+
action: "approve" | "dismiss" | "run_now" | "unsnooze",
|
|
246
|
+
) {
|
|
247
|
+
return openApiJson<OperationJson200<ReviewItemOperation>>(
|
|
248
|
+
reviewItemShape(),
|
|
249
|
+
(client, signal) => {
|
|
250
|
+
const request = {
|
|
251
|
+
params: { path: { item_id: id } },
|
|
252
|
+
headers: workspaceHeaders(),
|
|
253
|
+
signal,
|
|
254
|
+
};
|
|
255
|
+
if (action === "approve") return client.POST("/automation/reviews/{item_id}/approve", request);
|
|
256
|
+
if (action === "dismiss") return client.POST("/automation/reviews/{item_id}/dismiss", request);
|
|
257
|
+
if (action === "run_now") return client.POST("/automation/reviews/{item_id}/run_now", request);
|
|
258
|
+
return client.POST("/automation/reviews/{item_id}/unsnooze", request);
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function snoozeReview(id: string, until: string) {
|
|
264
|
+
return openApiJson<OperationJson200<operations["snooze_item_automation_reviews__item_id__snooze_post"]>>(
|
|
265
|
+
reviewItemShape(),
|
|
266
|
+
(client, signal) => client.POST("/automation/reviews/{item_id}/snooze", {
|
|
267
|
+
params: { path: { item_id: id } },
|
|
268
|
+
body: { until },
|
|
269
|
+
headers: workspaceHeaders(),
|
|
270
|
+
signal,
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
193
275
|
async function uploadDocument(file: File): Promise<ApiResult<Record<string, unknown> | null>> {
|
|
194
276
|
const base = await apiBase();
|
|
195
277
|
const form = new FormData();
|
|
@@ -409,13 +491,12 @@ export const latticeApi = {
|
|
|
409
491
|
hooks: () => get("/api/hooks", { hooks: [] }),
|
|
410
492
|
hookRuns: () => get("/api/hooks/runs", { runs: [] }, { limit: 50 }),
|
|
411
493
|
hookRun: (body: unknown) => post("/api/hooks/run", body, {}),
|
|
412
|
-
automationReviews:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
runNowReviewItem: (id: string) => post(`/automation/reviews/${encodeURIComponent(id)}/run_now`, {}, reviewItemShape()),
|
|
494
|
+
automationReviews: reviewList,
|
|
495
|
+
approveReviewItem: (id: string) => reviewAction(id, "approve"),
|
|
496
|
+
dismissReviewItem: (id: string) => reviewAction(id, "dismiss"),
|
|
497
|
+
snoozeReviewItem: snoozeReview,
|
|
498
|
+
unsnoozeReviewItem: (id: string) => reviewAction(id, "unsnooze"),
|
|
499
|
+
runNowReviewItem: (id: string) => reviewAction(id, "run_now"),
|
|
419
500
|
permissionsPending: () => get("/permissions/pending", { pending: {}, count: 0 }),
|
|
420
501
|
approvePermission: (token: string) => post(`/permissions/approve/${encodeURIComponent(token)}`, {}, {}),
|
|
421
502
|
denyPermission: (token: string) => post(`/permissions/deny/${encodeURIComponent(token)}`, {}, {}),
|
|
@@ -1914,6 +1914,23 @@ export interface paths {
|
|
|
1914
1914
|
patch?: never;
|
|
1915
1915
|
trace?: never;
|
|
1916
1916
|
};
|
|
1917
|
+
"/automation/reviews/{item_id}/unsnooze": {
|
|
1918
|
+
parameters: {
|
|
1919
|
+
query?: never;
|
|
1920
|
+
header?: never;
|
|
1921
|
+
path?: never;
|
|
1922
|
+
cookie?: never;
|
|
1923
|
+
};
|
|
1924
|
+
get?: never;
|
|
1925
|
+
put?: never;
|
|
1926
|
+
/** Unsnooze Item */
|
|
1927
|
+
post: operations["unsnooze_item_automation_reviews__item_id__unsnooze_post"];
|
|
1928
|
+
delete?: never;
|
|
1929
|
+
options?: never;
|
|
1930
|
+
head?: never;
|
|
1931
|
+
patch?: never;
|
|
1932
|
+
trace?: never;
|
|
1933
|
+
};
|
|
1917
1934
|
"/chat": {
|
|
1918
1935
|
parameters: {
|
|
1919
1936
|
query?: never;
|
|
@@ -11054,6 +11071,37 @@ export interface operations {
|
|
|
11054
11071
|
};
|
|
11055
11072
|
};
|
|
11056
11073
|
};
|
|
11074
|
+
unsnooze_item_automation_reviews__item_id__unsnooze_post: {
|
|
11075
|
+
parameters: {
|
|
11076
|
+
query?: never;
|
|
11077
|
+
header?: never;
|
|
11078
|
+
path: {
|
|
11079
|
+
item_id: string;
|
|
11080
|
+
};
|
|
11081
|
+
cookie?: never;
|
|
11082
|
+
};
|
|
11083
|
+
requestBody?: never;
|
|
11084
|
+
responses: {
|
|
11085
|
+
/** @description Successful Response */
|
|
11086
|
+
200: {
|
|
11087
|
+
headers: {
|
|
11088
|
+
[name: string]: unknown;
|
|
11089
|
+
};
|
|
11090
|
+
content: {
|
|
11091
|
+
"application/json": components["schemas"]["ReviewItem"];
|
|
11092
|
+
};
|
|
11093
|
+
};
|
|
11094
|
+
/** @description Validation Error */
|
|
11095
|
+
422: {
|
|
11096
|
+
headers: {
|
|
11097
|
+
[name: string]: unknown;
|
|
11098
|
+
};
|
|
11099
|
+
content: {
|
|
11100
|
+
"application/json": components["schemas"]["HTTPValidationError"];
|
|
11101
|
+
};
|
|
11102
|
+
};
|
|
11103
|
+
};
|
|
11104
|
+
};
|
|
11057
11105
|
chat_page_chat_get: {
|
|
11058
11106
|
parameters: {
|
|
11059
11107
|
query?: never;
|
|
@@ -53,10 +53,10 @@ export function FirstRunGuide() {
|
|
|
53
53
|
<section className="arrival-panel" aria-label="First 10 minutes">
|
|
54
54
|
<div className="arrival-copy">
|
|
55
55
|
<div className="page-kicker"><CheckCircle2 className="h-4 w-4" /> First 10 minutes</div>
|
|
56
|
-
<h2>
|
|
56
|
+
<h2>Start locally, with clear consent at each step.</h2>
|
|
57
57
|
<p>
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
Create the local Brain first, choose when to download a model, then add durable knowledge when you are ready.
|
|
59
|
+
Nothing needs cloud access unless you explicitly choose it.
|
|
60
60
|
</p>
|
|
61
61
|
<div className="arrival-actions">
|
|
62
62
|
<Button onClick={() => go(nextStep.action)}>{nextStep.done ? "Open relationships" : `Continue: ${nextStep.label}`}</Button>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RotateCcw } from "lucide-react";
|
|
2
|
+
import type { ApiResult, ReviewItem } from "@/api/client";
|
|
3
|
+
import { ActionButton, KeyValueList } from "@/components/primitives";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { useAppStore } from "@/store/appStore";
|
|
7
|
+
import {
|
|
8
|
+
formatSnoozedUntil,
|
|
9
|
+
hasRunBefore,
|
|
10
|
+
isActionableReview,
|
|
11
|
+
reviewSourceDetail,
|
|
12
|
+
reviewSourceLabel,
|
|
13
|
+
reviewStatusVariant,
|
|
14
|
+
type ReviewAction,
|
|
15
|
+
} from "./reviewHelpers";
|
|
16
|
+
|
|
17
|
+
type ReviewCardProps = {
|
|
18
|
+
item: ReviewItem;
|
|
19
|
+
feedback?: string;
|
|
20
|
+
onAction: (item: ReviewItem, action: ReviewAction, hadRunBefore?: boolean) => Promise<ApiResult<ReviewItem>>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
|
|
24
|
+
const mode = useAppStore((state) => state.mode);
|
|
25
|
+
const provenance = item.provenance || {};
|
|
26
|
+
const payload = item.payload || {};
|
|
27
|
+
const hadRun = hasRunBefore(item);
|
|
28
|
+
const snoozed = item.effective_status === "snoozed";
|
|
29
|
+
const actionable = isActionableReview(item);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="rounded-lg border border-border bg-background/55 p-4">
|
|
33
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
34
|
+
<div className="min-w-0 flex-1">
|
|
35
|
+
<div className="font-medium">{item.title}</div>
|
|
36
|
+
{item.summary ? <p className="mt-1 text-sm leading-6 text-muted-foreground">{item.summary}</p> : null}
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
39
|
+
<Badge variant="muted">{reviewSourceLabel(item.source)}</Badge>
|
|
40
|
+
<Badge variant={reviewStatusVariant(item.effective_status)}>{item.effective_status}</Badge>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{snoozed ? (
|
|
45
|
+
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-muted/24 p-3 text-sm">
|
|
46
|
+
<div>
|
|
47
|
+
<div className="font-medium">{formatSnoozedUntil(item.snoozed_until)}</div>
|
|
48
|
+
<p className="mt-1 text-muted-foreground">This stays out of the pending queue until then. Unsnooze brings it back immediately.</p>
|
|
49
|
+
</div>
|
|
50
|
+
<Button size="sm" variant="outline" onClick={() => onAction(item, "unsnooze")} disabled={!actionable}>
|
|
51
|
+
<RotateCcw className="h-3.5 w-3.5" /> Unsnooze
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
) : null}
|
|
55
|
+
|
|
56
|
+
{mode !== "basic" ? (
|
|
57
|
+
<div className="mt-3">
|
|
58
|
+
<KeyValueList
|
|
59
|
+
data={{
|
|
60
|
+
workflow: provenance.workflow_id,
|
|
61
|
+
trigger: provenance.trigger_id,
|
|
62
|
+
run: payload.last_run_id || provenance.run_id,
|
|
63
|
+
source_detail: reviewSourceDetail(provenance, item.source),
|
|
64
|
+
snoozed_until: item.snoozed_until,
|
|
65
|
+
created_at: item.created_at,
|
|
66
|
+
updated_at: item.updated_at,
|
|
67
|
+
}}
|
|
68
|
+
limit={8}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
) : null}
|
|
72
|
+
|
|
73
|
+
{actionable ? (
|
|
74
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
75
|
+
<ActionButton
|
|
76
|
+
label="Run now"
|
|
77
|
+
successLabel={hadRun ? "Regenerated" : "Executed"}
|
|
78
|
+
action={() => onAction(item, "run_now", hadRun)}
|
|
79
|
+
invalidate={[]}
|
|
80
|
+
/>
|
|
81
|
+
<ActionButton label="Approve" action={() => onAction(item, "approve")} invalidate={[]} />
|
|
82
|
+
{!snoozed ? <ActionButton label="Snooze 1 day" action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
|
|
83
|
+
<ActionButton label="Dismiss" action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
{feedback ? (
|
|
87
|
+
<p className="mt-2 text-xs text-emerald-300">{feedback} - item stays open until you approve or dismiss.</p>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { latticeApi, type ReviewItem, type ReviewSourceFilter, type ReviewStatusFilter } from "@/api/client";
|
|
4
|
+
import { EmptyState, LoadingPanel, Tabs } from "@/components/primitives";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import { ReviewCard } from "./ReviewCard";
|
|
8
|
+
import {
|
|
9
|
+
defaultSnoozeUntil,
|
|
10
|
+
reviewSourceFilters,
|
|
11
|
+
reviewStatusFilters,
|
|
12
|
+
type ReviewAction,
|
|
13
|
+
} from "./reviewHelpers";
|
|
14
|
+
|
|
15
|
+
export function ReviewInbox() {
|
|
16
|
+
const qc = useQueryClient();
|
|
17
|
+
const [statusFilter, setStatusFilter] = React.useState<ReviewStatusFilter>("pending");
|
|
18
|
+
const [sourceFilter, setSourceFilter] = React.useState<ReviewSourceFilter>("all");
|
|
19
|
+
const [runFeedback, setRunFeedback] = React.useState<Record<string, string>>({});
|
|
20
|
+
const reviews = useQuery({
|
|
21
|
+
queryKey: ["automationReviews", statusFilter, sourceFilter],
|
|
22
|
+
queryFn: () => latticeApi.automationReviews({
|
|
23
|
+
...(statusFilter !== "all" ? { status: statusFilter } : {}),
|
|
24
|
+
...(sourceFilter !== "all" ? { source: sourceFilter } : {}),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
const items = reviews.data?.data.items || [];
|
|
28
|
+
|
|
29
|
+
const actOnReview = async (
|
|
30
|
+
item: ReviewItem,
|
|
31
|
+
action: ReviewAction,
|
|
32
|
+
hadRunBefore = false,
|
|
33
|
+
) => {
|
|
34
|
+
const call =
|
|
35
|
+
action === "approve" ? () => latticeApi.approveReviewItem(item.id) :
|
|
36
|
+
action === "dismiss" ? () => latticeApi.dismissReviewItem(item.id) :
|
|
37
|
+
action === "snooze" ? () => latticeApi.snoozeReviewItem(item.id, defaultSnoozeUntil()) :
|
|
38
|
+
action === "unsnooze" ? () => latticeApi.unsnoozeReviewItem(item.id) :
|
|
39
|
+
() => latticeApi.runNowReviewItem(item.id);
|
|
40
|
+
const result = await call();
|
|
41
|
+
if (result.ok) {
|
|
42
|
+
if (action === "run_now") {
|
|
43
|
+
setRunFeedback((prev) => ({
|
|
44
|
+
...prev,
|
|
45
|
+
[item.id]: hadRunBefore ? "Regenerated" : "Executed",
|
|
46
|
+
}));
|
|
47
|
+
} else {
|
|
48
|
+
setRunFeedback((prev) => {
|
|
49
|
+
const next = { ...prev };
|
|
50
|
+
delete next[item.id];
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
await qc.invalidateQueries({ queryKey: ["automationReviews"] });
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (reviews.isLoading) return <LoadingPanel title="Review inbox" />;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card>
|
|
63
|
+
<CardHeader className="gap-3">
|
|
64
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
65
|
+
<div>
|
|
66
|
+
<CardTitle>Review inbox</CardTitle>
|
|
67
|
+
<CardDescription>Automation suggestions waiting for your decision. Run now executes without approving.</CardDescription>
|
|
68
|
+
</div>
|
|
69
|
+
{reviews.data ? (
|
|
70
|
+
<Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? "connected" : "unavailable"}</Badge>
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="grid gap-2">
|
|
74
|
+
<Tabs
|
|
75
|
+
tabs={reviewStatusFilters}
|
|
76
|
+
value={statusFilter}
|
|
77
|
+
onChange={(id) => setStatusFilter(id as ReviewStatusFilter)}
|
|
78
|
+
/>
|
|
79
|
+
<Tabs
|
|
80
|
+
tabs={reviewSourceFilters}
|
|
81
|
+
value={sourceFilter}
|
|
82
|
+
onChange={(id) => setSourceFilter(id as ReviewSourceFilter)}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</CardHeader>
|
|
86
|
+
<CardContent>
|
|
87
|
+
{reviews.isError || (reviews.data && !reviews.data.ok) ? (
|
|
88
|
+
<EmptyState
|
|
89
|
+
title="Could not load review inbox"
|
|
90
|
+
detail={reviews.data?.error || "The review queue is not available right now."}
|
|
91
|
+
/>
|
|
92
|
+
) : !items.length ? (
|
|
93
|
+
<EmptyState
|
|
94
|
+
title="Nothing to review"
|
|
95
|
+
detail={statusFilter === "snoozed" ? "Snoozed items will appear here until they are unsnoozed or become pending again." : "When automations opt into the review queue, new suggestions will appear here."}
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<div className="grid gap-3">
|
|
99
|
+
{items.map((item) => (
|
|
100
|
+
<ReviewCard
|
|
101
|
+
key={item.id}
|
|
102
|
+
item={item}
|
|
103
|
+
feedback={runFeedback[item.id]}
|
|
104
|
+
onAction={actOnReview}
|
|
105
|
+
/>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</CardContent>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
}
|