ltcai 6.3.0 → 6.3.1
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 +24 -15
- package/docs/CHANGELOG.md +24 -0
- package/frontend/src/features/review/ReviewCard.tsx +12 -10
- package/frontend/src/features/review/ReviewInbox.tsx +15 -10
- package/frontend/src/i18n.ts +116 -0
- package/frontend/src/pages/Capture.tsx +48 -41
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/app_factory.py +17 -67
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/access_runtime.py +97 -0
- 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-D76dWuQk.js → index-t1jx1BR9.js} +2 -2
- package/static/app/assets/index-t1jx1BR9.js.map +1 -0
- package/static/app/index.html +1 -1
- package/static/app/assets/index-D76dWuQk.js.map +0 -1
package/README.md
CHANGED
|
@@ -69,42 +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
101
|
|
|
102
102
|
### 6. Review Center
|
|
103
103
|
|
|
104
104
|
Automation results are staged for review before they become durable decisions.
|
|
105
105
|
Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
|
|
106
106
|
|
|
107
|
-

|
|
108
108
|
|
|
109
109
|
## Brain Depths
|
|
110
110
|
|
|
@@ -120,10 +120,10 @@ The user travels inward from everyday memory to deeper structure:
|
|
|
120
120
|
|
|
121
121
|
Walkthrough:
|
|
122
122
|
|
|
123
|
-

|
|
124
124
|
|
|
125
125
|
Screenshot index and capture notes:
|
|
126
|
-
[output/release/v6.3.
|
|
126
|
+
[output/release/v6.3.1/SCREENSHOT_INDEX.md](output/release/v6.3.1/SCREENSHOT_INDEX.md)
|
|
127
127
|
|
|
128
128
|
## Install
|
|
129
129
|
|
|
@@ -200,7 +200,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
|
|
|
200
200
|
|
|
201
201
|
## Current Release Preparation
|
|
202
202
|
|
|
203
|
-
The current development target is **6.3.
|
|
203
|
+
The current development target is **6.3.1 Access Runtime / i18n Follow-up**:
|
|
204
204
|
|
|
205
205
|
- `App.tsx` now delegates the main Brain and Admin surfaces to focused feature
|
|
206
206
|
modules instead of owning the full product shell.
|
|
@@ -220,14 +220,19 @@ The current development target is **6.3.0 Product Hardening Completion**:
|
|
|
220
220
|
- Brain archive, memory provenance, document ingestion, Review Center Run Now,
|
|
221
221
|
local model runtime, root-shim smoke, and app-factory review wiring now have
|
|
222
222
|
dedicated UX/backend hardening coverage for the 6.3 line.
|
|
223
|
+
- Access-control helper closures are extracted from `app_factory.py` into a
|
|
224
|
+
focused runtime module with direct unit coverage for role lookup, token
|
|
225
|
+
extraction, user enforcement, admin enforcement, and public user projection.
|
|
226
|
+
- Capture and Review Center copy now route through the shared Korean/English
|
|
227
|
+
i18n map for tabs, placeholders, action labels, empty states, and feedback.
|
|
223
228
|
|
|
224
|
-
Expected artifacts for 6.3.
|
|
229
|
+
Expected artifacts for 6.3.1 release must use exact filenames:
|
|
225
230
|
|
|
226
|
-
- `dist/ltcai-6.3.
|
|
227
|
-
- `dist/ltcai-6.3.
|
|
228
|
-
- `ltcai-6.3.
|
|
229
|
-
- `dist/ltcai-6.3.
|
|
230
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.
|
|
231
|
+
- `dist/ltcai-6.3.1-py3-none-any.whl`
|
|
232
|
+
- `dist/ltcai-6.3.1.tar.gz`
|
|
233
|
+
- `ltcai-6.3.1.tgz`
|
|
234
|
+
- `dist/ltcai-6.3.1.vsix`
|
|
235
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.1_aarch64.dmg`
|
|
231
236
|
|
|
232
237
|
Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
233
238
|
|
|
@@ -246,6 +251,10 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
|
246
251
|
|
|
247
252
|
| Version | Theme |
|
|
248
253
|
| --- | --- |
|
|
254
|
+
| 6.3.1 | Access Runtime / i18n Follow-up: app-factory access-control extraction, focused access runtime tests, and Capture/Review Center i18n coverage |
|
|
255
|
+
| 6.3.0 | Product Hardening Completion: Brain archive/provenance/ingestion UX polish, Review Center Run Now contract hardening, local model runtime status, app-factory review wiring, shim smoke, i18n guard, and exact release artifacts |
|
|
256
|
+
| 6.2.0 | Product Decomposition / Release Smoke Automation: App and ProductFlow feature extraction, legacy root shim shrink, model download consent UX, typed router contexts, localized smoke tests, and wheel/npm/static/Tauri release smoke |
|
|
257
|
+
| 6.1.0 | Product Hardening / Digital Brain Completion: local Brain entry without a loaded model, Brain memory/backup loop, Review Center semantics, ToolRegistry authorization boundary, and root CLI/runtime compatibility |
|
|
249
258
|
| 6.0.0 | Product Reset / Review Center Completion: Snoozed filter, Unsnooze, OpenAPI-derived Review typing, Review feature extraction, v6 docs and scorecard |
|
|
250
259
|
| 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 |
|
|
251
260
|
| 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 |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.3.1] - 2026-06-17
|
|
7
|
+
|
|
8
|
+
> Access Runtime / i18n Follow-up. Closes the next app-factory decomposition
|
|
9
|
+
> slice and finishes Capture/Review Center localization coverage on top of the
|
|
10
|
+
> v6.3.0 product-hardening line.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Moved app-factory access-control helper closures into
|
|
14
|
+
`latticeai.runtime.access_runtime` while preserving role lookup, token
|
|
15
|
+
extraction, user enforcement, admin enforcement, and public user projection
|
|
16
|
+
call signatures.
|
|
17
|
+
- Capture tabs, headings, placeholders, action labels, upload details, folder
|
|
18
|
+
connection copy, browser capture copy, and pipeline controls now use the
|
|
19
|
+
shared Korean/English i18n map.
|
|
20
|
+
- Review Center inbox/card copy, status badges, empty states, Run Now feedback,
|
|
21
|
+
snooze/unsnooze labels, and action aria labels now use the shared i18n map.
|
|
22
|
+
- README release evidence screenshots and walkthrough GIF are refreshed under
|
|
23
|
+
`output/release/v6.3.1/`.
|
|
24
|
+
- Package/runtime/static metadata is synchronized to 6.3.1; package publish and
|
|
25
|
+
deployment remain owner-run only.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Focused unit coverage for the extracted access runtime auth/admin behavior.
|
|
29
|
+
|
|
6
30
|
## [6.3.0] - 2026-06-16
|
|
7
31
|
|
|
8
32
|
> Product Hardening Completion. Completes the 6.2 decomposition follow-up by
|
|
@@ -3,6 +3,7 @@ import type { ApiResult, ReviewItem } from "@/api/client";
|
|
|
3
3
|
import { ActionButton, KeyValueList } from "@/components/primitives";
|
|
4
4
|
import { Badge } from "@/components/ui/badge";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { t } from "@/i18n";
|
|
6
7
|
import { useAppStore } from "@/store/appStore";
|
|
7
8
|
import {
|
|
8
9
|
formatSnoozedUntil,
|
|
@@ -22,6 +23,7 @@ type ReviewCardProps = {
|
|
|
22
23
|
|
|
23
24
|
export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
|
|
24
25
|
const mode = useAppStore((state) => state.mode);
|
|
26
|
+
const language = useAppStore((state) => state.language);
|
|
25
27
|
const provenance = item.provenance || {};
|
|
26
28
|
const payload = item.payload || {};
|
|
27
29
|
const hadRun = hasRunBefore(item);
|
|
@@ -45,10 +47,10 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
|
|
|
45
47
|
<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
48
|
<div>
|
|
47
49
|
<div className="font-medium">{formatSnoozedUntil(item.snoozed_until)}</div>
|
|
48
|
-
<p className="mt-1 text-muted-foreground">
|
|
50
|
+
<p className="mt-1 text-muted-foreground">{t(language, "review.snoozed.detail")}</p>
|
|
49
51
|
</div>
|
|
50
52
|
<Button size="sm" variant="outline" onClick={() => onAction(item, "unsnooze")} disabled={!actionable}>
|
|
51
|
-
<RotateCcw className="h-3.5 w-3.5" />
|
|
53
|
+
<RotateCcw className="h-3.5 w-3.5" /> {t(language, "review.unsnooze")}
|
|
52
54
|
</Button>
|
|
53
55
|
</div>
|
|
54
56
|
) : null}
|
|
@@ -73,24 +75,24 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
|
|
|
73
75
|
{actionable ? (
|
|
74
76
|
<div className="mt-4 grid gap-2">
|
|
75
77
|
<p className="text-xs leading-5 text-muted-foreground">
|
|
76
|
-
|
|
78
|
+
{t(language, "review.runNow.detail")}
|
|
77
79
|
</p>
|
|
78
|
-
<div className="flex flex-wrap gap-2" aria-label="
|
|
80
|
+
<div className="flex flex-wrap gap-2" aria-label={t(language, "review.actions.aria")}>
|
|
79
81
|
<ActionButton
|
|
80
|
-
label=
|
|
81
|
-
successLabel={hadRun ? "
|
|
82
|
+
label={t(language, "review.runNow")}
|
|
83
|
+
successLabel={hadRun ? t(language, "review.regenerated") : t(language, "review.executed")}
|
|
82
84
|
action={() => onAction(item, "run_now", hadRun)}
|
|
83
85
|
invalidate={[]}
|
|
84
86
|
/>
|
|
85
|
-
<ActionButton label="
|
|
86
|
-
{!snoozed ? <ActionButton label=
|
|
87
|
-
<ActionButton label="
|
|
87
|
+
<ActionButton label={t(language, "review.approve")} action={() => onAction(item, "approve")} invalidate={[]} />
|
|
88
|
+
{!snoozed ? <ActionButton label={t(language, "review.snoozeDay")} action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
|
|
89
|
+
<ActionButton label={t(language, "review.dismiss")} action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
|
|
88
90
|
</div>
|
|
89
91
|
</div>
|
|
90
92
|
) : null}
|
|
91
93
|
{feedback ? (
|
|
92
94
|
<p className={`mt-2 text-xs ${/fail|error|unavailable/i.test(feedback) ? "text-amber-300" : "text-emerald-300"}`}>
|
|
93
|
-
{feedback} -
|
|
95
|
+
{feedback} - {t(language, "review.feedback.open")}
|
|
94
96
|
</p>
|
|
95
97
|
) : null}
|
|
96
98
|
</div>
|
|
@@ -4,6 +4,8 @@ import { latticeApi, type ReviewItem, type ReviewSourceFilter, type ReviewStatus
|
|
|
4
4
|
import { EmptyState, LoadingPanel, Tabs } from "@/components/primitives";
|
|
5
5
|
import { Badge } from "@/components/ui/badge";
|
|
6
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import { t } from "@/i18n";
|
|
8
|
+
import { useAppStore } from "@/store/appStore";
|
|
7
9
|
import { ReviewCard } from "./ReviewCard";
|
|
8
10
|
import {
|
|
9
11
|
defaultSnoozeUntil,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
} from "./reviewHelpers";
|
|
14
16
|
|
|
15
17
|
export function ReviewInbox() {
|
|
18
|
+
const language = useAppStore((state) => state.language);
|
|
16
19
|
const qc = useQueryClient();
|
|
17
20
|
const [statusFilter, setStatusFilter] = React.useState<ReviewStatusFilter>("pending");
|
|
18
21
|
const [sourceFilter, setSourceFilter] = React.useState<ReviewSourceFilter>("all");
|
|
@@ -41,7 +44,7 @@ export function ReviewInbox() {
|
|
|
41
44
|
if (!result.ok) {
|
|
42
45
|
setRunFeedback((prev) => ({
|
|
43
46
|
...prev,
|
|
44
|
-
[item.id]: result.error ||
|
|
47
|
+
[item.id]: result.error || t(language, "review.action.failed", { action }),
|
|
45
48
|
}));
|
|
46
49
|
return result;
|
|
47
50
|
}
|
|
@@ -52,7 +55,9 @@ export function ReviewInbox() {
|
|
|
52
55
|
const runId = String(payload.last_run_id || provenance.run_id || "");
|
|
53
56
|
setRunFeedback((prev) => ({
|
|
54
57
|
...prev,
|
|
55
|
-
[item.id]: runId
|
|
58
|
+
[item.id]: runId
|
|
59
|
+
? `${hadRunBefore ? t(language, "review.regenerated") : t(language, "review.executed")} · ${runId}`
|
|
60
|
+
: hadRunBefore ? t(language, "review.regenerated") : t(language, "review.executed"),
|
|
56
61
|
}));
|
|
57
62
|
} else {
|
|
58
63
|
setRunFeedback((prev) => {
|
|
@@ -66,18 +71,18 @@ export function ReviewInbox() {
|
|
|
66
71
|
return result;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
|
-
if (reviews.isLoading) return <LoadingPanel title="
|
|
74
|
+
if (reviews.isLoading) return <LoadingPanel title={t(language, "review.inbox.title")} />;
|
|
70
75
|
|
|
71
76
|
return (
|
|
72
77
|
<Card>
|
|
73
78
|
<CardHeader className="gap-3">
|
|
74
79
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
75
80
|
<div>
|
|
76
|
-
<CardTitle>
|
|
77
|
-
<CardDescription>
|
|
81
|
+
<CardTitle>{t(language, "review.inbox.title")}</CardTitle>
|
|
82
|
+
<CardDescription>{t(language, "review.inbox.description")}</CardDescription>
|
|
78
83
|
</div>
|
|
79
84
|
{reviews.data ? (
|
|
80
|
-
<Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? "connected" : "unavailable"}</Badge>
|
|
85
|
+
<Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? t(language, "review.status.connected") : t(language, "review.status.unavailable")}</Badge>
|
|
81
86
|
) : null}
|
|
82
87
|
</div>
|
|
83
88
|
<div className="grid gap-2">
|
|
@@ -96,13 +101,13 @@ export function ReviewInbox() {
|
|
|
96
101
|
<CardContent>
|
|
97
102
|
{reviews.isError || (reviews.data && !reviews.data.ok) ? (
|
|
98
103
|
<EmptyState
|
|
99
|
-
title="
|
|
100
|
-
detail={reviews.data?.error || "
|
|
104
|
+
title={t(language, "review.inbox.loadError")}
|
|
105
|
+
detail={reviews.data?.error || t(language, "review.inbox.unavailable")}
|
|
101
106
|
/>
|
|
102
107
|
) : !items.length ? (
|
|
103
108
|
<EmptyState
|
|
104
|
-
title="
|
|
105
|
-
detail={statusFilter === "snoozed" ? "
|
|
109
|
+
title={t(language, "review.inbox.empty")}
|
|
110
|
+
detail={statusFilter === "snoozed" ? t(language, "review.inbox.emptySnoozed") : t(language, "review.inbox.emptyPending")}
|
|
106
111
|
/>
|
|
107
112
|
) : (
|
|
108
113
|
<div className="grid gap-3">
|
package/frontend/src/i18n.ts
CHANGED
|
@@ -109,6 +109,64 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
109
109
|
"care.result.path": "파일: {path}",
|
|
110
110
|
"care.result.contents": "기억 {memories}개 · 연결 {links}개 · 대화 {conversations}개",
|
|
111
111
|
"care.result.dryRun": "미리보기만 실행되어 현재 Brain은 바뀌지 않았습니다.",
|
|
112
|
+
"capture.tab.files": "파일",
|
|
113
|
+
"capture.tab.local": "폴더",
|
|
114
|
+
"capture.tab.browser": "웹",
|
|
115
|
+
"capture.tab.pipeline": "흐름",
|
|
116
|
+
"capture.kicker": "추가",
|
|
117
|
+
"capture.title": "중요한 자료를 Brain에 넣으세요.",
|
|
118
|
+
"capture.body": "파일을 넣고, 폴더를 연결하고, 웹페이지를 저장하세요. Lattice는 배운 생각의 출처를 함께 기억합니다.",
|
|
119
|
+
"capture.retry": "다시 시도",
|
|
120
|
+
"capture.files.title": "문서 추가",
|
|
121
|
+
"capture.files.description": "파일을 선택하면 Lattice가 검색과 기억에 들어갈 수 있게 준비합니다.",
|
|
122
|
+
"capture.files.drop": "파일을 놓거나 문서를 선택",
|
|
123
|
+
"capture.files.dropDetail": "각 파일은 대기열에 들어가고, 파싱된 뒤 Memory에 저장되며, 출처 메타데이터와 함께 Brain 그래프에 연결됩니다.",
|
|
124
|
+
"capture.files.uploaded": "업로드된 문서",
|
|
125
|
+
"capture.files.completed": "완료된 업로드는 Memory에 들어간 뒤 여기에 표시됩니다. 그래프 링크는 파싱 후 잠시 더 만들어질 수 있습니다.",
|
|
126
|
+
"capture.files.success": "Brain과 그래프 대기열에 들어감",
|
|
127
|
+
"capture.files.queued": "수집 대기열에서 대기 중",
|
|
128
|
+
"capture.files.uploading": "파싱 후 Memory로 전송 중",
|
|
129
|
+
"capture.files.failed": "Brain에 들어가기 전에 수집에 실패했습니다.",
|
|
130
|
+
"capture.files.captured": "출처 메타데이터와 함께 수집됨",
|
|
131
|
+
"capture.files.capturedWithNode": "출처 메타데이터와 함께 수집됨 · {node}",
|
|
132
|
+
"capture.local.title": "폴더 연결",
|
|
133
|
+
"capture.local.description": "Lattice가 기억해야 할 폴더를 지정하세요.",
|
|
134
|
+
"capture.local.placeholder": "이 Mac의 폴더 경로",
|
|
135
|
+
"capture.local.connect": "폴더 연결",
|
|
136
|
+
"capture.local.success": "폴더 연결 요청됨",
|
|
137
|
+
"capture.local.sources": "연결된 출처",
|
|
138
|
+
"capture.local.access": "폴더 접근",
|
|
139
|
+
"capture.browser.title": "웹페이지 저장",
|
|
140
|
+
"capture.browser.description": "유용한 내용을 기억할 수 있게 페이지를 수집합니다.",
|
|
141
|
+
"capture.browser.placeholder": "https://example.com/article",
|
|
142
|
+
"capture.browser.capture": "URL 수집",
|
|
143
|
+
"capture.browser.success": "URL 수집 요청됨",
|
|
144
|
+
"capture.pipeline.status": "처리 상태",
|
|
145
|
+
"capture.pipeline.growth": "Brain 성장",
|
|
146
|
+
"capture.pipeline.refresh": "기억 새로고침",
|
|
147
|
+
"capture.pipeline.description": "수집한 자료를 Lattice가 다시 확인해야 할 때 검색을 새로고침합니다.",
|
|
148
|
+
"capture.pipeline.rebuild": "검색 인덱스 다시 만들기",
|
|
149
|
+
"review.inbox.title": "검토함",
|
|
150
|
+
"review.inbox.description": "결정 대기 중인 자동화 제안입니다. 지금 실행은 승인 없이 미리보기만 수행합니다.",
|
|
151
|
+
"review.status.connected": "연결됨",
|
|
152
|
+
"review.status.unavailable": "사용 불가",
|
|
153
|
+
"review.inbox.loadError": "검토함을 불러올 수 없습니다.",
|
|
154
|
+
"review.inbox.unavailable": "지금은 검토 대기열을 사용할 수 없습니다.",
|
|
155
|
+
"review.inbox.empty": "검토할 항목 없음",
|
|
156
|
+
"review.inbox.emptySnoozed": "일시 중지된 항목은 해제되거나 다시 대기 상태가 될 때까지 여기에 표시됩니다.",
|
|
157
|
+
"review.inbox.emptyPending": "자동화가 검토 대기열을 사용하면 새 제안이 여기에 표시됩니다.",
|
|
158
|
+
"review.snoozed.detail": "해당 시점까지 대기열에서 빠져 있습니다. 일시 중지 해제 시 즉시 돌아옵니다.",
|
|
159
|
+
"review.unsnooze": "일시 중지 해제",
|
|
160
|
+
"review.runNow.detail": "지금 실행은 승인 없이 작업을 미리봅니다. 결과가 맞으면 승인하거나 기각하세요.",
|
|
161
|
+
"review.actions.aria": "검토 작업",
|
|
162
|
+
"review.runNow": "지금 실행",
|
|
163
|
+
"review.regenerated": "다시 생성됨",
|
|
164
|
+
"review.executed": "실행됨",
|
|
165
|
+
"review.approve": "승인",
|
|
166
|
+
"review.snoozeDay": "하루 일시 중지",
|
|
167
|
+
"review.dismiss": "기각",
|
|
168
|
+
"review.feedback.open": "승인 또는 기각 전까지 항목은 열린 상태로 유지됩니다.",
|
|
169
|
+
"review.action.failed": "{action} 실패",
|
|
112
170
|
"admin.back": "Brain",
|
|
113
171
|
"admin.aria.console": "Lattice 관리자",
|
|
114
172
|
"admin.kicker": "분리된 관리자 작업공간",
|
|
@@ -372,6 +430,64 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
372
430
|
"care.result.path": "File: {path}",
|
|
373
431
|
"care.result.contents": "{memories} memories · {links} links · {conversations} conversations",
|
|
374
432
|
"care.result.dryRun": "Preview only; your current Brain was not changed.",
|
|
433
|
+
"capture.tab.files": "Files",
|
|
434
|
+
"capture.tab.local": "Folders",
|
|
435
|
+
"capture.tab.browser": "Web",
|
|
436
|
+
"capture.tab.pipeline": "Flow",
|
|
437
|
+
"capture.kicker": "Add",
|
|
438
|
+
"capture.title": "Feed the brain what matters.",
|
|
439
|
+
"capture.body": "Drop in files, connect folders, or save a page. Lattice remembers the origin of every idea it learns.",
|
|
440
|
+
"capture.retry": "Retry",
|
|
441
|
+
"capture.files.title": "Add documents",
|
|
442
|
+
"capture.files.description": "Choose files and Lattice will prepare them for search and memory.",
|
|
443
|
+
"capture.files.drop": "Drop files or choose documents",
|
|
444
|
+
"capture.files.dropDetail": "Each file is queued, parsed, written to Memory, and linked into the Brain graph with source metadata.",
|
|
445
|
+
"capture.files.uploaded": "Uploaded documents",
|
|
446
|
+
"capture.files.completed": "Completed uploads appear here after they enter Memory. Graph links may continue building briefly after parsing finishes.",
|
|
447
|
+
"capture.files.success": "Entered Brain and graph queue",
|
|
448
|
+
"capture.files.queued": "Waiting in the ingest queue",
|
|
449
|
+
"capture.files.uploading": "Parsing and sending to Memory",
|
|
450
|
+
"capture.files.failed": "Ingest failed before it entered the Brain",
|
|
451
|
+
"capture.files.captured": "Captured with source metadata",
|
|
452
|
+
"capture.files.capturedWithNode": "Captured with source metadata · {node}",
|
|
453
|
+
"capture.local.title": "Connect a folder",
|
|
454
|
+
"capture.local.description": "Point Lattice at a folder you want it to remember.",
|
|
455
|
+
"capture.local.placeholder": "Folder path on this Mac",
|
|
456
|
+
"capture.local.connect": "Connect Folder",
|
|
457
|
+
"capture.local.success": "Folder connection requested",
|
|
458
|
+
"capture.local.sources": "Connected sources",
|
|
459
|
+
"capture.local.access": "Folder access",
|
|
460
|
+
"capture.browser.title": "Save a web page",
|
|
461
|
+
"capture.browser.description": "Capture a page so Lattice can remember the useful parts.",
|
|
462
|
+
"capture.browser.placeholder": "https://example.com/article",
|
|
463
|
+
"capture.browser.capture": "Capture URL",
|
|
464
|
+
"capture.browser.success": "URL capture requested",
|
|
465
|
+
"capture.pipeline.status": "Processing status",
|
|
466
|
+
"capture.pipeline.growth": "Brain growth",
|
|
467
|
+
"capture.pipeline.refresh": "Refresh memory",
|
|
468
|
+
"capture.pipeline.description": "Refresh search when you want Lattice to re-check captured material.",
|
|
469
|
+
"capture.pipeline.rebuild": "Rebuild retrieval index",
|
|
470
|
+
"review.inbox.title": "Review inbox",
|
|
471
|
+
"review.inbox.description": "Automation suggestions waiting for your decision. Run now previews without approving.",
|
|
472
|
+
"review.status.connected": "connected",
|
|
473
|
+
"review.status.unavailable": "unavailable",
|
|
474
|
+
"review.inbox.loadError": "Could not load review inbox",
|
|
475
|
+
"review.inbox.unavailable": "The review queue is not available right now.",
|
|
476
|
+
"review.inbox.empty": "Nothing to review",
|
|
477
|
+
"review.inbox.emptySnoozed": "Snoozed items will appear here until they are unsnoozed or become pending again.",
|
|
478
|
+
"review.inbox.emptyPending": "When automations opt into the review queue, new suggestions will appear here.",
|
|
479
|
+
"review.snoozed.detail": "This stays out of the pending queue until then. Unsnooze brings it back immediately.",
|
|
480
|
+
"review.unsnooze": "Unsnooze",
|
|
481
|
+
"review.runNow.detail": "Run now previews the action without approving it. Approve or dismiss when the result looks right.",
|
|
482
|
+
"review.actions.aria": "Review actions",
|
|
483
|
+
"review.runNow": "Run now",
|
|
484
|
+
"review.regenerated": "Regenerated",
|
|
485
|
+
"review.executed": "Executed",
|
|
486
|
+
"review.approve": "Approve",
|
|
487
|
+
"review.snoozeDay": "Snooze 1 day",
|
|
488
|
+
"review.dismiss": "Dismiss",
|
|
489
|
+
"review.feedback.open": "item stays open until you approve or dismiss.",
|
|
490
|
+
"review.action.failed": "{action} failed",
|
|
375
491
|
"admin.back": "Brain",
|
|
376
492
|
"admin.aria.console": "Lattice Admin",
|
|
377
493
|
"admin.kicker": "Separate admin workspace",
|
|
@@ -6,28 +6,30 @@ import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, T
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
8
|
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { t, type Language } from "@/i18n";
|
|
9
10
|
import { asArray } from "@/lib/utils";
|
|
11
|
+
import { useAppStore } from "@/store/appStore";
|
|
10
12
|
|
|
11
13
|
type CaptureTab = "files" | "local" | "browser" | "pipeline";
|
|
12
14
|
|
|
13
|
-
const tabs: Array<{ id: CaptureTab; label: string }> = [
|
|
14
|
-
{ id: "files", label: "Files" },
|
|
15
|
-
{ id: "local", label: "Folders" },
|
|
16
|
-
{ id: "browser", label: "Web" },
|
|
17
|
-
{ id: "pipeline", label: "Flow" },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
15
|
export function CapturePage({ initialTab }: { initialTab?: string }) {
|
|
16
|
+
const language = useAppStore((state) => state.language);
|
|
21
17
|
const [tab, setTab] = React.useState<CaptureTab>((initialTab as CaptureTab) || "files");
|
|
18
|
+
const tabs: Array<{ id: CaptureTab; label: string }> = [
|
|
19
|
+
{ id: "files", label: t(language, "capture.tab.files") },
|
|
20
|
+
{ id: "local", label: t(language, "capture.tab.local") },
|
|
21
|
+
{ id: "browser", label: t(language, "capture.tab.browser") },
|
|
22
|
+
{ id: "pipeline", label: t(language, "capture.tab.pipeline") },
|
|
23
|
+
];
|
|
22
24
|
React.useEffect(() => {
|
|
23
25
|
if (initialTab === "pipeline" || initialTab === "local" || initialTab === "files") setTab(initialTab);
|
|
24
26
|
}, [initialTab]);
|
|
25
27
|
return (
|
|
26
28
|
<div className="space-y-5">
|
|
27
29
|
<header className="page-hero">
|
|
28
|
-
<div className="page-kicker"><Upload className="h-4 w-4" />
|
|
29
|
-
<h1 className="page-title">
|
|
30
|
-
<p className="page-copy">
|
|
30
|
+
<div className="page-kicker"><Upload className="h-4 w-4" /> {t(language, "capture.kicker")}</div>
|
|
31
|
+
<h1 className="page-title">{t(language, "capture.title")}</h1>
|
|
32
|
+
<p className="page-copy">{t(language, "capture.body")}</p>
|
|
31
33
|
</header>
|
|
32
34
|
<Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as CaptureTab)} />
|
|
33
35
|
{tab === "files" ? <FilesPanel /> : null}
|
|
@@ -39,6 +41,7 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function FilesPanel() {
|
|
44
|
+
const language = useAppStore((state) => state.language);
|
|
42
45
|
const qc = useQueryClient();
|
|
43
46
|
const docs = useQuery({ queryKey: ["documents"], queryFn: () => latticeApi.documents(200) });
|
|
44
47
|
const [queue, setQueue] = React.useState<UploadQueueItem[]>([]);
|
|
@@ -59,8 +62,8 @@ function FilesPanel() {
|
|
|
59
62
|
<div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
|
|
60
63
|
<Card>
|
|
61
64
|
<CardHeader>
|
|
62
|
-
<CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" />
|
|
63
|
-
<CardDescription>
|
|
65
|
+
<CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> {t(language, "capture.files.title")}</CardTitle>
|
|
66
|
+
<CardDescription>{t(language, "capture.files.description")}</CardDescription>
|
|
64
67
|
</CardHeader>
|
|
65
68
|
<CardContent>
|
|
66
69
|
<label
|
|
@@ -72,19 +75,19 @@ function FilesPanel() {
|
|
|
72
75
|
}}
|
|
73
76
|
>
|
|
74
77
|
<Upload className="h-7 w-7 text-primary" />
|
|
75
|
-
<span className="text-lg font-semibold">
|
|
76
|
-
<span className="max-w-sm text-sm leading-6 text-muted-foreground">
|
|
78
|
+
<span className="text-lg font-semibold">{t(language, "capture.files.drop")}</span>
|
|
79
|
+
<span className="max-w-sm text-sm leading-6 text-muted-foreground">{t(language, "capture.files.dropDetail")}</span>
|
|
77
80
|
<input type="file" multiple className="sr-only" onChange={(e) => e.target.files && beginUpload(e.target.files)} />
|
|
78
81
|
</label>
|
|
79
82
|
<DocumentUploadQueue queue={queue} onRetry={(file) => beginUpload([file])} />
|
|
80
83
|
</CardContent>
|
|
81
84
|
</Card>
|
|
82
|
-
<DataPanel title=
|
|
85
|
+
<DataPanel title={t(language, "capture.files.uploaded")} result={docs.data}>
|
|
83
86
|
{(data) => (
|
|
84
87
|
<div className="space-y-3">
|
|
85
88
|
<EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />
|
|
86
89
|
<div className="rounded-md border border-border bg-background/55 p-3 text-sm text-muted-foreground">
|
|
87
|
-
|
|
90
|
+
{t(language, "capture.files.completed")}
|
|
88
91
|
</div>
|
|
89
92
|
</div>
|
|
90
93
|
)}
|
|
@@ -122,11 +125,12 @@ async function uploadFiles(files: File[], setQueue: React.Dispatch<React.SetStat
|
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onRetry: (file: File) => void }) {
|
|
128
|
+
const language = useAppStore((state) => state.language);
|
|
125
129
|
if (!queue.length) return null;
|
|
126
130
|
return (
|
|
127
131
|
<div className="mt-4 space-y-2">
|
|
128
132
|
{queue.map((item) => {
|
|
129
|
-
const detail = uploadResultDetail(item);
|
|
133
|
+
const detail = uploadResultDetail(item, language);
|
|
130
134
|
return (
|
|
131
135
|
<div key={item.id} className="rounded-md border border-border bg-background/55 p-3 text-sm">
|
|
132
136
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
@@ -141,11 +145,11 @@ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onR
|
|
|
141
145
|
</div>
|
|
142
146
|
{item.status === "failed" ? (
|
|
143
147
|
<Button size="sm" variant="outline" onClick={() => onRetry(item.file)}>
|
|
144
|
-
<RotateCcw className="h-3.5 w-3.5" />
|
|
148
|
+
<RotateCcw className="h-3.5 w-3.5" /> {t(language, "capture.retry")}
|
|
145
149
|
</Button>
|
|
146
150
|
) : null}
|
|
147
151
|
</div>
|
|
148
|
-
{item.result ? <OperationResult result={item.result} successLabel=
|
|
152
|
+
{item.result ? <OperationResult result={item.result} successLabel={t(language, "capture.files.success")} /> : null}
|
|
149
153
|
</div>
|
|
150
154
|
);
|
|
151
155
|
})}
|
|
@@ -153,16 +157,17 @@ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onR
|
|
|
153
157
|
);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
function uploadResultDetail(item: UploadQueueItem) {
|
|
157
|
-
if (item.status === "queued") return
|
|
158
|
-
if (item.status === "uploading") return
|
|
159
|
-
if (!item.result?.ok) return item.result?.error || "
|
|
160
|
+
function uploadResultDetail(item: UploadQueueItem, language: Language) {
|
|
161
|
+
if (item.status === "queued") return t(language, "capture.files.queued");
|
|
162
|
+
if (item.status === "uploading") return t(language, "capture.files.uploading");
|
|
163
|
+
if (!item.result?.ok) return item.result?.error || t(language, "capture.files.failed");
|
|
160
164
|
const data = item.result.data || {};
|
|
161
165
|
const node = String(data.node_id || data.graph_node || data.provenance_id || "");
|
|
162
|
-
return node ?
|
|
166
|
+
return node ? t(language, "capture.files.capturedWithNode", { node }) : t(language, "capture.files.captured");
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
function LocalPanel() {
|
|
170
|
+
const language = useAppStore((state) => state.language);
|
|
166
171
|
const qc = useQueryClient();
|
|
167
172
|
const [path, setPath] = React.useState("");
|
|
168
173
|
const local = useQuery({ queryKey: ["localSources"], queryFn: latticeApi.localSources });
|
|
@@ -175,16 +180,16 @@ function LocalPanel() {
|
|
|
175
180
|
<div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
|
176
181
|
<Card>
|
|
177
182
|
<CardHeader>
|
|
178
|
-
<CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" />
|
|
179
|
-
<CardDescription>
|
|
183
|
+
<CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> {t(language, "capture.local.title")}</CardTitle>
|
|
184
|
+
<CardDescription>{t(language, "capture.local.description")}</CardDescription>
|
|
180
185
|
</CardHeader>
|
|
181
186
|
<CardContent className="space-y-3">
|
|
182
|
-
<Input value={path} onChange={(e) => setPath(e.target.value)} placeholder=
|
|
183
|
-
<Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>
|
|
184
|
-
{connect.data ? <OperationResult result={connect.data} successLabel=
|
|
187
|
+
<Input value={path} onChange={(e) => setPath(e.target.value)} placeholder={t(language, "capture.local.placeholder")} />
|
|
188
|
+
<Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>{t(language, "capture.local.connect")}</Button>
|
|
189
|
+
{connect.data ? <OperationResult result={connect.data} successLabel={t(language, "capture.local.success")} /> : null}
|
|
185
190
|
</CardContent>
|
|
186
191
|
</Card>
|
|
187
|
-
<DataPanel title="
|
|
192
|
+
<DataPanel title={t(language, "capture.local.sources")} result={local.data}>
|
|
188
193
|
{(data) => (
|
|
189
194
|
<div className="space-y-3">
|
|
190
195
|
<EntityList items={(data as Record<string, unknown>).sources} titleKey="path" metaKey="status" />
|
|
@@ -199,7 +204,7 @@ function LocalPanel() {
|
|
|
199
204
|
</div>
|
|
200
205
|
)}
|
|
201
206
|
</DataPanel>
|
|
202
|
-
<DataPanel title="
|
|
207
|
+
<DataPanel title={t(language, "capture.local.access")} result={agent.data} className="xl:col-span-2">
|
|
203
208
|
{(data) => <StructuredView value={data} />}
|
|
204
209
|
</DataPanel>
|
|
205
210
|
</div>
|
|
@@ -207,43 +212,45 @@ function LocalPanel() {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
function BrowserPanel() {
|
|
215
|
+
const language = useAppStore((state) => state.language);
|
|
210
216
|
const [url, setUrl] = React.useState("");
|
|
211
217
|
const read = useMutation({ mutationFn: () => latticeApi.browserReadUrl(url) });
|
|
212
218
|
return (
|
|
213
219
|
<Card>
|
|
214
220
|
<CardHeader>
|
|
215
|
-
<CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" />
|
|
216
|
-
<CardDescription>
|
|
221
|
+
<CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> {t(language, "capture.browser.title")}</CardTitle>
|
|
222
|
+
<CardDescription>{t(language, "capture.browser.description")}</CardDescription>
|
|
217
223
|
</CardHeader>
|
|
218
224
|
<CardContent className="space-y-3">
|
|
219
225
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
220
|
-
<Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="
|
|
221
|
-
<Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>
|
|
226
|
+
<Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder={t(language, "capture.browser.placeholder")} />
|
|
227
|
+
<Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>{t(language, "capture.browser.capture")}</Button>
|
|
222
228
|
</div>
|
|
223
|
-
{read.data ? <OperationResult result={read.data} successLabel="
|
|
229
|
+
{read.data ? <OperationResult result={read.data} successLabel={t(language, "capture.browser.success")} /> : null}
|
|
224
230
|
</CardContent>
|
|
225
231
|
</Card>
|
|
226
232
|
);
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
function PipelinePanel() {
|
|
236
|
+
const language = useAppStore((state) => state.language);
|
|
230
237
|
const index = useQuery({ queryKey: ["index"], queryFn: latticeApi.indexStatus });
|
|
231
238
|
const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
|
|
232
239
|
return (
|
|
233
240
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
234
|
-
<DataPanel title="
|
|
241
|
+
<DataPanel title={t(language, "capture.pipeline.status")} result={index.data}>
|
|
235
242
|
{(data) => <StructuredView value={data} />}
|
|
236
243
|
</DataPanel>
|
|
237
|
-
<DataPanel title="
|
|
244
|
+
<DataPanel title={t(language, "capture.pipeline.growth")} result={stats.data}>
|
|
238
245
|
{(data) => <StructuredView value={data} />}
|
|
239
246
|
</DataPanel>
|
|
240
247
|
<Card className="xl:col-span-2">
|
|
241
248
|
<CardHeader>
|
|
242
|
-
<CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" />
|
|
243
|
-
<CardDescription>
|
|
249
|
+
<CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> {t(language, "capture.pipeline.refresh")}</CardTitle>
|
|
250
|
+
<CardDescription>{t(language, "capture.pipeline.description")}</CardDescription>
|
|
244
251
|
</CardHeader>
|
|
245
252
|
<CardContent>
|
|
246
|
-
<ActionButton label=
|
|
253
|
+
<ActionButton label={t(language, "capture.pipeline.rebuild")} action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
|
|
247
254
|
</CardContent>
|
|
248
255
|
</Card>
|
|
249
256
|
</div>
|