ltcai 6.2.0 → 6.3.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 +18 -15
- package/docs/CHANGELOG.md +42 -0
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
- package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
- package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
- package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
- package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
- package/frontend/src/features/admin/AdminConsole.tsx +1 -1
- package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
- package/frontend/src/features/brain/BrainComposer.tsx +66 -0
- package/frontend/src/features/brain/BrainConversation.tsx +131 -0
- package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
- package/frontend/src/features/brain/BrainHome.tsx +28 -795
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
- package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
- package/frontend/src/features/brain/types.ts +6 -6
- package/frontend/src/features/review/ReviewCard.tsx +3 -1
- package/frontend/src/features/review/ReviewInbox.tsx +11 -1
- package/frontend/src/i18n.ts +92 -0
- package/frontend/src/pages/Brain.tsx +63 -5
- package/frontend/src/pages/Capture.tsx +102 -13
- package/frontend/src/pages/Library.tsx +72 -6
- 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 +31 -39
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/chat_wiring.py +119 -0
- package/latticeai/runtime/model_wiring.py +40 -0
- package/latticeai/runtime/platform_runtime_wiring.py +2 -3
- package/latticeai/runtime/review_wiring.py +37 -0
- package/latticeai/runtime/tail_wiring.py +21 -0
- package/package.json +2 -2
- package/scripts/check_i18n_literals.mjs +52 -0
- 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 +5 -5
- package/static/app/assets/{index-D91Rz5--.js → index-D76dWuQk.js} +2 -2
- package/static/app/assets/index-D76dWuQk.js.map +1 -0
- package/static/app/assets/index-Div5vMlq.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-B2-1Gm0q.css +0 -2
- package/static/app/assets/index-D91Rz5--.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BrainDepth, MemoryFragment } from "./types";
|
|
2
|
+
import { t } from "@/i18n";
|
|
3
|
+
import { useAppStore } from "@/store/appStore";
|
|
4
|
+
import { layerStyle, polarPoint } from "./graphLayout";
|
|
5
|
+
|
|
6
|
+
export function BrainMemoryLayer({
|
|
7
|
+
memories,
|
|
8
|
+
depth,
|
|
9
|
+
onRecallMemory,
|
|
10
|
+
}: {
|
|
11
|
+
memories: MemoryFragment[];
|
|
12
|
+
depth: BrainDepth;
|
|
13
|
+
onRecallMemory: (fragment: MemoryFragment) => void;
|
|
14
|
+
}) {
|
|
15
|
+
const language = useAppStore((state) => state.language);
|
|
16
|
+
const visible = memories.slice(0, depth >= 3 ? 8 : 6);
|
|
17
|
+
if (!visible.length) return <div className="memory-fragment is-empty">{t(language, "brain.memory.empty")}</div>;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{visible.map((memory, index) => {
|
|
22
|
+
const point = polarPoint(index, visible.length, depth >= 3 ? 39 : 31, depth >= 3 ? 24 : 18, -112);
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
key={memory.id}
|
|
26
|
+
type="button"
|
|
27
|
+
className="memory-fragment"
|
|
28
|
+
style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 55}ms` })}
|
|
29
|
+
onClick={() => onRecallMemory(memory)}
|
|
30
|
+
>
|
|
31
|
+
<span>{memory.kind}</span>
|
|
32
|
+
<strong>{memory.title}</strong>
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { t } from "@/i18n";
|
|
3
|
+
import { useAppStore } from "@/store/appStore";
|
|
4
|
+
import type { BrainDepth, KnowledgeConcept, MemoryFragment } from "./types";
|
|
5
|
+
|
|
6
|
+
export function BrainOverviewPanel({
|
|
7
|
+
memories,
|
|
8
|
+
concepts,
|
|
9
|
+
onOpenDepth,
|
|
10
|
+
}: {
|
|
11
|
+
memories: MemoryFragment[];
|
|
12
|
+
concepts: KnowledgeConcept[];
|
|
13
|
+
onOpenDepth: (depth: BrainDepth) => void;
|
|
14
|
+
}) {
|
|
15
|
+
const language = useAppStore((state) => state.language);
|
|
16
|
+
const recent = memories.slice(0, 3);
|
|
17
|
+
const older = memories.slice(3, 6);
|
|
18
|
+
const topics = concepts.slice(0, 4);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<section className="brain-overview-panel" aria-label={t(language, "brain.aria.overview")}>
|
|
22
|
+
<div className="brain-overview-head">
|
|
23
|
+
<div>
|
|
24
|
+
<span>{t(language, "brain.overview.kicker")}</span>
|
|
25
|
+
<strong>{t(language, "brain.overview.title")}</strong>
|
|
26
|
+
</div>
|
|
27
|
+
<button type="button" onClick={() => onOpenDepth(5)}>{t(language, "brain.overview.graph")}</button>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="brain-overview-grid">
|
|
30
|
+
<BrainOverviewColumn
|
|
31
|
+
title={t(language, "brain.overview.recent")}
|
|
32
|
+
empty={t(language, "brain.overview.recentEmpty")}
|
|
33
|
+
items={recent.map((memory) => memory.title)}
|
|
34
|
+
onOpen={() => onOpenDepth(2)}
|
|
35
|
+
/>
|
|
36
|
+
<BrainOverviewColumn
|
|
37
|
+
title={t(language, "brain.overview.older")}
|
|
38
|
+
empty={t(language, "brain.overview.olderEmpty")}
|
|
39
|
+
items={older.map((memory) => memory.title)}
|
|
40
|
+
onOpen={() => onOpenDepth(2)}
|
|
41
|
+
/>
|
|
42
|
+
<BrainOverviewColumn
|
|
43
|
+
title={t(language, "brain.overview.topics")}
|
|
44
|
+
empty={t(language, "brain.overview.topicsEmpty")}
|
|
45
|
+
items={topics.map((concept) => concept.label)}
|
|
46
|
+
onOpen={() => onOpenDepth(3)}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function BrainOverviewColumn({
|
|
54
|
+
title,
|
|
55
|
+
empty,
|
|
56
|
+
items,
|
|
57
|
+
onOpen,
|
|
58
|
+
}: {
|
|
59
|
+
title: string;
|
|
60
|
+
empty: string;
|
|
61
|
+
items: string[];
|
|
62
|
+
onOpen: () => void;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<button type="button" className="brain-overview-column" onClick={onOpen}>
|
|
66
|
+
<span>{title}</span>
|
|
67
|
+
{items.length ? (
|
|
68
|
+
items.slice(0, 3).map((item) => <strong key={item}>{item}</strong>)
|
|
69
|
+
) : (
|
|
70
|
+
<em>{empty}</em>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { KnowledgeConcept, RelationshipThread } from "./types";
|
|
2
|
+
import { layoutGraphNodes } from "./graphLayout";
|
|
3
|
+
|
|
4
|
+
export function BrainRelationshipLayer({
|
|
5
|
+
concepts,
|
|
6
|
+
relationships,
|
|
7
|
+
}: {
|
|
8
|
+
concepts: KnowledgeConcept[];
|
|
9
|
+
relationships: RelationshipThread[];
|
|
10
|
+
}) {
|
|
11
|
+
const visibleConcepts = concepts.slice(0, 10);
|
|
12
|
+
const layout = layoutGraphNodes(visibleConcepts, 30, 20);
|
|
13
|
+
const positionById = new Map(layout.map((item) => [item.node.id, item]));
|
|
14
|
+
const visibleRelationships = relationships
|
|
15
|
+
.map((relationship, index) => {
|
|
16
|
+
const source = positionById.get(relationship.source) || layout[index % Math.max(layout.length, 1)];
|
|
17
|
+
const target = positionById.get(relationship.target) || layout[(index + 3) % Math.max(layout.length, 1)];
|
|
18
|
+
return source && target && source.node.id !== target.node.id ? { relationship, source, target } : null;
|
|
19
|
+
})
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.slice(0, 8) as Array<{
|
|
22
|
+
relationship: RelationshipThread;
|
|
23
|
+
source: ReturnType<typeof layoutGraphNodes>[number];
|
|
24
|
+
target: ReturnType<typeof layoutGraphNodes>[number];
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
if (!visibleRelationships.length) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<svg className="relationship-weave" viewBox="0 0 100 100" aria-hidden>
|
|
31
|
+
{visibleRelationships.map(({ relationship, source, target }, index) => (
|
|
32
|
+
<line
|
|
33
|
+
key={`${relationship.id}-${index}`}
|
|
34
|
+
x1={source.x}
|
|
35
|
+
y1={source.y}
|
|
36
|
+
x2={target.x}
|
|
37
|
+
y2={target.y}
|
|
38
|
+
style={{ animationDelay: `${index * 80}ms` }}
|
|
39
|
+
/>
|
|
40
|
+
))}
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BrainDepth, KnowledgeConcept, KnowledgeGraphModel, MemoryFragment, RelationshipThread } from "./types";
|
|
2
|
+
import { BrainGraphLayer, BrainKnowledgeLayer } from "./BrainGraphLayer";
|
|
3
|
+
import { BrainMemoryLayer } from "./BrainMemoryLayer";
|
|
4
|
+
import { BrainRelationshipLayer } from "./BrainRelationshipLayer";
|
|
5
|
+
|
|
6
|
+
export function DepthEmergence({
|
|
7
|
+
depth,
|
|
8
|
+
memories,
|
|
9
|
+
concepts,
|
|
10
|
+
relationships,
|
|
11
|
+
graphModel,
|
|
12
|
+
graphSearch,
|
|
13
|
+
selectedGraphId,
|
|
14
|
+
onGraphSearch,
|
|
15
|
+
onSelectGraphNode,
|
|
16
|
+
onRecallMemory,
|
|
17
|
+
}: {
|
|
18
|
+
depth: BrainDepth;
|
|
19
|
+
memories: MemoryFragment[];
|
|
20
|
+
concepts: KnowledgeConcept[];
|
|
21
|
+
relationships: RelationshipThread[];
|
|
22
|
+
graphModel: KnowledgeGraphModel;
|
|
23
|
+
graphSearch: string;
|
|
24
|
+
selectedGraphId: string | null;
|
|
25
|
+
onGraphSearch: (value: string) => void;
|
|
26
|
+
onSelectGraphNode: (id: string | null) => void;
|
|
27
|
+
onRecallMemory: (fragment: MemoryFragment) => void;
|
|
28
|
+
}) {
|
|
29
|
+
if (depth === 1) return null;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{depth >= 2 ? (
|
|
34
|
+
<BrainMemoryLayer memories={memories} depth={depth} onRecallMemory={onRecallMemory} />
|
|
35
|
+
) : null}
|
|
36
|
+
{depth >= 3 && depth < 5 ? (
|
|
37
|
+
<BrainKnowledgeLayer concepts={concepts} depth={depth} />
|
|
38
|
+
) : null}
|
|
39
|
+
{depth >= 4 && depth < 5 ? (
|
|
40
|
+
<BrainRelationshipLayer concepts={concepts} relationships={relationships} />
|
|
41
|
+
) : null}
|
|
42
|
+
{depth >= 5 ? (
|
|
43
|
+
<BrainGraphLayer
|
|
44
|
+
model={graphModel}
|
|
45
|
+
search={graphSearch}
|
|
46
|
+
selectedId={selectedGraphId}
|
|
47
|
+
onSearch={onGraphSearch}
|
|
48
|
+
onSelect={onSelectGraphNode}
|
|
49
|
+
/>
|
|
50
|
+
) : null}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -35,10 +35,10 @@ export type KnowledgeGraphModel = {
|
|
|
35
35
|
edges: RelationshipThread[];
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export const DEPTHS: Array<{ level: BrainDepth;
|
|
39
|
-
{ level: 1,
|
|
40
|
-
{ level: 2,
|
|
41
|
-
{ level: 3,
|
|
42
|
-
{ level: 4,
|
|
43
|
-
{ level: 5,
|
|
38
|
+
export const DEPTHS: Array<{ level: BrainDepth; labelKey: string; state: BrainState }> = [
|
|
39
|
+
{ level: 1, labelKey: "brain.depthLabel.1", state: "idle" },
|
|
40
|
+
{ level: 2, labelKey: "brain.depthLabel.2", state: "recalling" },
|
|
41
|
+
{ level: 3, labelKey: "brain.depthLabel.3", state: "synthesizing" },
|
|
42
|
+
{ level: 4, labelKey: "brain.depthLabel.4", state: "planning" },
|
|
43
|
+
{ level: 5, labelKey: "brain.depthLabel.5", state: "synthesizing" },
|
|
44
44
|
];
|
|
@@ -89,7 +89,9 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
|
|
|
89
89
|
</div>
|
|
90
90
|
) : null}
|
|
91
91
|
{feedback ? (
|
|
92
|
-
<p className=
|
|
92
|
+
<p className={`mt-2 text-xs ${/fail|error|unavailable/i.test(feedback) ? "text-amber-300" : "text-emerald-300"}`}>
|
|
93
|
+
{feedback} - item stays open until you approve or dismiss.
|
|
94
|
+
</p>
|
|
93
95
|
) : null}
|
|
94
96
|
</div>
|
|
95
97
|
);
|
|
@@ -38,11 +38,21 @@ export function ReviewInbox() {
|
|
|
38
38
|
action === "unsnooze" ? () => latticeApi.unsnoozeReviewItem(item.id) :
|
|
39
39
|
() => latticeApi.runNowReviewItem(item.id);
|
|
40
40
|
const result = await call();
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
setRunFeedback((prev) => ({
|
|
43
|
+
...prev,
|
|
44
|
+
[item.id]: result.error || `${action} failed`,
|
|
45
|
+
}));
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
41
48
|
if (result.ok) {
|
|
42
49
|
if (action === "run_now") {
|
|
50
|
+
const payload = result.data.payload || {};
|
|
51
|
+
const provenance = result.data.provenance || {};
|
|
52
|
+
const runId = String(payload.last_run_id || provenance.run_id || "");
|
|
43
53
|
setRunFeedback((prev) => ({
|
|
44
54
|
...prev,
|
|
45
|
-
[item.id]: hadRunBefore ? "Regenerated" : "Executed",
|
|
55
|
+
[item.id]: runId ? `${hadRunBefore ? "Regenerated" : "Executed"} · ${runId}` : hadRunBefore ? "Regenerated" : "Executed",
|
|
46
56
|
}));
|
|
47
57
|
} else {
|
|
48
58
|
setRunFeedback((prev) => {
|
package/frontend/src/i18n.ts
CHANGED
|
@@ -18,11 +18,24 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
18
18
|
"brain.depth.3": "주제",
|
|
19
19
|
"brain.depth.4": "관계",
|
|
20
20
|
"brain.depth.5": "전체 지식 그래프",
|
|
21
|
+
"brain.depthLabel.1": "살아있는 Brain",
|
|
22
|
+
"brain.depthLabel.2": "기억 계층",
|
|
23
|
+
"brain.depthLabel.3": "지식 계층",
|
|
24
|
+
"brain.depthLabel.4": "관계 계층",
|
|
25
|
+
"brain.depthLabel.5": "지식 그래프",
|
|
21
26
|
"brain.view.memories": "기억 보기",
|
|
22
27
|
"brain.view.topics": "주제 보기",
|
|
23
28
|
"brain.view.relationships": "관계 보기",
|
|
24
29
|
"brain.view.graph": "그래프로 보기",
|
|
25
30
|
"brain.surface": "돌아가기",
|
|
31
|
+
"brain.aria.home": "Lattice Brain",
|
|
32
|
+
"brain.aria.exploration": "Brain 탐색",
|
|
33
|
+
"brain.aria.quickViews": "Brain 빠른 보기",
|
|
34
|
+
"brain.aria.conversation": "대화",
|
|
35
|
+
"brain.aria.ownership": "Brain 소유 보장",
|
|
36
|
+
"brain.aria.starterPrompts": "시작 프롬프트",
|
|
37
|
+
"brain.aria.overview": "Brain 한눈에 보기",
|
|
38
|
+
"brain.aria.graph": "지식 그래프",
|
|
26
39
|
"brain.title": "Lattice Brain",
|
|
27
40
|
"brain.local": "로컬 우선",
|
|
28
41
|
"brain.portable": "이동 가능",
|
|
@@ -54,28 +67,50 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
54
67
|
"brain.overview.olderEmpty": "대화가 쌓이면 과거 기억이 보입니다.",
|
|
55
68
|
"brain.overview.topics": "주요 주제",
|
|
56
69
|
"brain.overview.topicsEmpty": "주제가 형성되는 중입니다.",
|
|
70
|
+
"brain.memory.empty": "기억이 조용합니다.",
|
|
71
|
+
"brain.knowledge.empty": "지식이 형성되는 중입니다.",
|
|
57
72
|
"brain.graph.empty": "아직 맞는 지식이 없습니다.",
|
|
58
73
|
"brain.graph.summaryFallback": "이 개념은 가장 깊은 지식 계층의 일부입니다.",
|
|
59
74
|
"brain.graph.focused": "대화와 문서에서 함께 나온 내용이 선으로 이어집니다.",
|
|
60
75
|
"brain.graph.emptyFocus": "대화, 문서, 프로젝트를 쌓으면 Brain 그래프가 자랍니다.",
|
|
76
|
+
"brain.graph.search": "검색",
|
|
77
|
+
"brain.graph.searchAria": "지식 그래프 검색",
|
|
61
78
|
"care.title": "내 Brain 돌보기",
|
|
62
79
|
"care.subtitle": "내 컴퓨터에 두고, 필요하면 옮길 수 있습니다.",
|
|
63
80
|
"care.private": "개인 보관",
|
|
81
|
+
"care.ownershipModel": "소유 모델",
|
|
64
82
|
"care.export": "내보내기",
|
|
65
83
|
"care.export.detail": "다른 곳으로 가져가기",
|
|
66
84
|
"care.backup": "백업",
|
|
67
85
|
"care.backup.detail": "복사본 저장",
|
|
86
|
+
"care.backup.count": "백업 {count}개",
|
|
87
|
+
"care.backup.ready": "백업 준비됨",
|
|
68
88
|
"care.archive": "보관 파일",
|
|
69
89
|
"care.archive.detail": "암호화된 Brain",
|
|
70
90
|
"care.path.placeholder": "확인하거나 미리 볼 보관 파일 경로",
|
|
71
91
|
"care.path.label": "Brain 보관 파일 경로",
|
|
72
92
|
"care.passphrase.placeholder": "보관 파일 비밀번호",
|
|
73
93
|
"care.passphrase.label": "Brain 보관 파일 비밀번호",
|
|
94
|
+
"care.passphrase.confirmPlaceholder": "비밀번호 다시 입력",
|
|
95
|
+
"care.passphrase.confirmLabel": "Brain 보관 파일 비밀번호 확인",
|
|
96
|
+
"care.passphrase.tooShort": "비밀번호는 12자 이상이어야 합니다.",
|
|
97
|
+
"care.passphrase.tooSimple": "대문자, 소문자, 숫자, 기호 중 3종류 이상을 섞어 주세요.",
|
|
98
|
+
"care.passphrase.mismatch": "비밀번호 확인이 일치하지 않습니다.",
|
|
99
|
+
"care.passphrase.strong": "비밀번호 강도가 충분합니다.",
|
|
100
|
+
"care.path.ready": "보관 파일 경로 형식이 올바릅니다.",
|
|
101
|
+
"care.path.invalidChars": "보관 파일 경로에 사용할 수 없는 문자가 있습니다.",
|
|
102
|
+
"care.path.extension": ".latticebrain, .zip, .json 파일만 확인할 수 있습니다.",
|
|
74
103
|
"care.inspect": "확인",
|
|
75
104
|
"care.restorePreview": "복원 미리보기",
|
|
76
105
|
"care.note": "복원 미리보기는 Brain을 바꾸지 않고 보관 파일만 확인합니다. 실제 복원은 설정에서 확정합니다.",
|
|
77
106
|
"care.working": "진행 중",
|
|
107
|
+
"care.result.error": "Brain 관리 작업을 완료하지 못했습니다.",
|
|
108
|
+
"care.result.completed": "Brain 관리 작업 완료",
|
|
109
|
+
"care.result.path": "파일: {path}",
|
|
110
|
+
"care.result.contents": "기억 {memories}개 · 연결 {links}개 · 대화 {conversations}개",
|
|
111
|
+
"care.result.dryRun": "미리보기만 실행되어 현재 Brain은 바뀌지 않았습니다.",
|
|
78
112
|
"admin.back": "Brain",
|
|
113
|
+
"admin.aria.console": "Lattice 관리자",
|
|
79
114
|
"admin.kicker": "분리된 관리자 작업공간",
|
|
80
115
|
"admin.title": "Admin Console",
|
|
81
116
|
"admin.body": "사용자, 로그, 보안, Brain 상태는 일반 사용자 화면과 분리됩니다.",
|
|
@@ -173,7 +208,9 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
173
208
|
"flow.login.title": "내 AI 브레인의 주인을 정합니다.",
|
|
174
209
|
"flow.login.body": "Lattice AI는 내 지식과 맥락을 이 컴퓨터에 보관하는 로컬 우선 AI 브레인입니다. 모델은 바꿀 수 있고, 외부 전송은 사용자가 선택할 때만 시작됩니다.",
|
|
175
210
|
"flow.name": "이름",
|
|
211
|
+
"flow.name.placeholder": "나",
|
|
176
212
|
"flow.email": "이메일",
|
|
213
|
+
"flow.email.placeholder": "you@local",
|
|
177
214
|
"flow.password": "비밀번호",
|
|
178
215
|
"flow.password.placeholder": "로컬 Brain 비밀번호",
|
|
179
216
|
"flow.login.busy": "Brain 여는 중...",
|
|
@@ -189,6 +226,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
189
226
|
"flow.promise.model.v": "모델은 목소리이고, 자산은 Brain입니다.",
|
|
190
227
|
"flow.promise.ownership.k": "사용자 소유",
|
|
191
228
|
"flow.promise.ownership.v": "백업, 복원, 이동을 직접 결정합니다.",
|
|
229
|
+
"flow.promise.aria": "Lattice AI 제품 약속",
|
|
192
230
|
"flow.analysis.title": "이 컴퓨터에서 가능한 경험을 확인합니다.",
|
|
193
231
|
"flow.analysis.body": "스펙 점수가 아니라, 이 Mac에서 어떤 로컬 AI 브레인 경험이 편한지 알려드립니다. 확인은 로컬 상태를 읽는 것이고, 클라우드 모델은 선택 사항입니다.",
|
|
194
232
|
"flow.analysis.finding": "가장 편한 설정을 찾는 중...",
|
|
@@ -196,6 +234,10 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
196
234
|
"flow.analysis.wait": "잠시만 기다리면 자동으로 정리됩니다.",
|
|
197
235
|
"flow.analysis.error": "Lattice가 이 컴퓨터를 끝까지 확인하지 못했습니다. 그래도 안전한 기본값으로 계속할 수 있습니다.",
|
|
198
236
|
"flow.analysis.continue": "추천 모델 보기",
|
|
237
|
+
"flow.analysis.bestFit": "{model}이 이 컴퓨터에 가장 잘 맞습니다.",
|
|
238
|
+
"flow.analysis.privateRecommended": "이 컴퓨터에는 개인 로컬 Brain을 추천합니다.",
|
|
239
|
+
"flow.analysis.os.windows": "Windows PC",
|
|
240
|
+
"flow.analysis.os.linux": "Linux 컴퓨터",
|
|
199
241
|
"flow.recommend.title": "추천대로 시작하세요.",
|
|
200
242
|
"flow.recommend.body": "모델은 Brain의 현재 목소리입니다. 가장 안전한 추천, 더 빠른 모델, 더 강한 모델만 먼저 보여드립니다.",
|
|
201
243
|
"flow.recommend.primary": "추천대로 시작하기",
|
|
@@ -203,6 +245,10 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
203
245
|
"flow.recommend.back": "뒤로",
|
|
204
246
|
"flow.recommend.skip": "모델 없이 Brain 열기",
|
|
205
247
|
"flow.recommend.hint": "잘 모르겠다면 추천대로 시작하면 됩니다.",
|
|
248
|
+
"flow.recommend.rank.best": "추천",
|
|
249
|
+
"flow.recommend.rank.faster": "빠른 선택",
|
|
250
|
+
"flow.recommend.rank.advanced": "고급 선택",
|
|
251
|
+
"flow.recommend.rank.choice": "선택 {index}",
|
|
206
252
|
"flow.install.title": "모델을 설치하고 시작합니다.",
|
|
207
253
|
"flow.install.body": "이 모델이 Brain의 로컬 목소리가 됩니다. 인터넷은 다운로드할 때만 필요하고, 실행과 기억은 이 컴퓨터에서 유지됩니다.",
|
|
208
254
|
"flow.install.wait": "Brain이 사용할 모델을 기다리고 있습니다.",
|
|
@@ -235,11 +281,24 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
235
281
|
"brain.depth.3": "Topics",
|
|
236
282
|
"brain.depth.4": "Relationships",
|
|
237
283
|
"brain.depth.5": "Full knowledge graph",
|
|
284
|
+
"brain.depthLabel.1": "Living Brain",
|
|
285
|
+
"brain.depthLabel.2": "Memory Layer",
|
|
286
|
+
"brain.depthLabel.3": "Knowledge Layer",
|
|
287
|
+
"brain.depthLabel.4": "Relationship Layer",
|
|
288
|
+
"brain.depthLabel.5": "Knowledge Graph",
|
|
238
289
|
"brain.view.memories": "Memories",
|
|
239
290
|
"brain.view.topics": "Topics",
|
|
240
291
|
"brain.view.relationships": "Relationships",
|
|
241
292
|
"brain.view.graph": "Graph",
|
|
242
293
|
"brain.surface": "Back",
|
|
294
|
+
"brain.aria.home": "Lattice Brain",
|
|
295
|
+
"brain.aria.exploration": "Brain exploration",
|
|
296
|
+
"brain.aria.quickViews": "Brain quick views",
|
|
297
|
+
"brain.aria.conversation": "Conversation",
|
|
298
|
+
"brain.aria.ownership": "Brain ownership guarantees",
|
|
299
|
+
"brain.aria.starterPrompts": "Starter prompts",
|
|
300
|
+
"brain.aria.overview": "Brain overview",
|
|
301
|
+
"brain.aria.graph": "Knowledge Graph",
|
|
243
302
|
"brain.title": "Lattice Brain",
|
|
244
303
|
"brain.local": "Local-first",
|
|
245
304
|
"brain.portable": "Portable",
|
|
@@ -271,28 +330,50 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
271
330
|
"brain.overview.olderEmpty": "Earlier memories appear as conversations accumulate.",
|
|
272
331
|
"brain.overview.topics": "Main topics",
|
|
273
332
|
"brain.overview.topicsEmpty": "Topics are still forming.",
|
|
333
|
+
"brain.memory.empty": "Memory is quiet",
|
|
334
|
+
"brain.knowledge.empty": "Knowledge is forming",
|
|
274
335
|
"brain.graph.empty": "No matching knowledge yet",
|
|
275
336
|
"brain.graph.summaryFallback": "This concept is part of the deepest knowledge layer.",
|
|
276
337
|
"brain.graph.focused": "Ideas that appeared together in conversations and documents are connected by lines.",
|
|
277
338
|
"brain.graph.emptyFocus": "Your Brain graph grows as you add conversations, documents, and projects.",
|
|
339
|
+
"brain.graph.search": "Search",
|
|
340
|
+
"brain.graph.searchAria": "Search knowledge graph",
|
|
278
341
|
"care.title": "Care for my Brain",
|
|
279
342
|
"care.subtitle": "Own it locally. Keep it portable.",
|
|
280
343
|
"care.private": "Private",
|
|
344
|
+
"care.ownershipModel": "Ownership model",
|
|
281
345
|
"care.export": "Export",
|
|
282
346
|
"care.export.detail": "Take it with you",
|
|
283
347
|
"care.backup": "Backup",
|
|
284
348
|
"care.backup.detail": "Save a copy",
|
|
349
|
+
"care.backup.count": "{count} backups",
|
|
350
|
+
"care.backup.ready": "Backups ready",
|
|
285
351
|
"care.archive": "Archive",
|
|
286
352
|
"care.archive.detail": "Encrypted Brain",
|
|
287
353
|
"care.path.placeholder": "Paste an archive path to inspect or preview",
|
|
288
354
|
"care.path.label": "Brain archive path",
|
|
289
355
|
"care.passphrase.placeholder": "Archive passphrase",
|
|
290
356
|
"care.passphrase.label": "Brain archive passphrase",
|
|
357
|
+
"care.passphrase.confirmPlaceholder": "Repeat passphrase",
|
|
358
|
+
"care.passphrase.confirmLabel": "Confirm Brain archive passphrase",
|
|
359
|
+
"care.passphrase.tooShort": "Use at least 12 characters.",
|
|
360
|
+
"care.passphrase.tooSimple": "Mix at least three of uppercase, lowercase, numbers, and symbols.",
|
|
361
|
+
"care.passphrase.mismatch": "Passphrases do not match.",
|
|
362
|
+
"care.passphrase.strong": "Passphrase strength is sufficient.",
|
|
363
|
+
"care.path.ready": "Archive path format looks valid.",
|
|
364
|
+
"care.path.invalidChars": "Archive path contains unsupported characters.",
|
|
365
|
+
"care.path.extension": "Use a .latticebrain, .zip, or .json archive.",
|
|
291
366
|
"care.inspect": "Inspect",
|
|
292
367
|
"care.restorePreview": "Restore preview",
|
|
293
368
|
"care.note": "Restore preview checks an archive without changing your Brain. Confirmed restore stays in Settings.",
|
|
294
369
|
"care.working": "Working",
|
|
370
|
+
"care.result.error": "Brain care action could not complete.",
|
|
371
|
+
"care.result.completed": "Brain care action completed.",
|
|
372
|
+
"care.result.path": "File: {path}",
|
|
373
|
+
"care.result.contents": "{memories} memories · {links} links · {conversations} conversations",
|
|
374
|
+
"care.result.dryRun": "Preview only; your current Brain was not changed.",
|
|
295
375
|
"admin.back": "Brain",
|
|
376
|
+
"admin.aria.console": "Lattice Admin",
|
|
296
377
|
"admin.kicker": "Separate admin workspace",
|
|
297
378
|
"admin.title": "Admin Console",
|
|
298
379
|
"admin.body": "Users, logs, security, and Brain health stay out of the normal user experience.",
|
|
@@ -390,7 +471,9 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
390
471
|
"flow.login.title": "Choose the owner of your AI Brain.",
|
|
391
472
|
"flow.login.body": "Lattice AI is a local-first Digital Brain that keeps your knowledge on this computer. Models can change; external transfer starts only when you choose it.",
|
|
392
473
|
"flow.name": "Name",
|
|
474
|
+
"flow.name.placeholder": "You",
|
|
393
475
|
"flow.email": "Email",
|
|
476
|
+
"flow.email.placeholder": "you@local",
|
|
394
477
|
"flow.password": "Password",
|
|
395
478
|
"flow.password.placeholder": "Local Brain password",
|
|
396
479
|
"flow.login.busy": "Opening Brain...",
|
|
@@ -406,6 +489,7 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
406
489
|
"flow.promise.model.v": "The model is the voice; the Brain is the asset.",
|
|
407
490
|
"flow.promise.ownership.k": "User owned",
|
|
408
491
|
"flow.promise.ownership.v": "You decide when to back up, restore, or move it.",
|
|
492
|
+
"flow.promise.aria": "Lattice AI product promise",
|
|
409
493
|
"flow.analysis.title": "Checking what this computer can do.",
|
|
410
494
|
"flow.analysis.body": "This is not a spec test. Lattice reads local capability and explains what AI Brain experience this Mac can support. Cloud models remain optional.",
|
|
411
495
|
"flow.analysis.finding": "Finding the easiest setup...",
|
|
@@ -413,6 +497,10 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
413
497
|
"flow.analysis.wait": "This will be summarized automatically in a moment.",
|
|
414
498
|
"flow.analysis.error": "Lattice could not finish checking this computer. You can still continue with a safe default.",
|
|
415
499
|
"flow.analysis.continue": "View recommended models",
|
|
500
|
+
"flow.analysis.bestFit": "{model} looks like the best fit.",
|
|
501
|
+
"flow.analysis.privateRecommended": "A private local Brain is recommended for this computer.",
|
|
502
|
+
"flow.analysis.os.windows": "Windows PC",
|
|
503
|
+
"flow.analysis.os.linux": "Linux computer",
|
|
416
504
|
"flow.recommend.title": "Start with the recommendation.",
|
|
417
505
|
"flow.recommend.body": "The model is your Brain's current voice. You see the safest recommendation, a faster model, and a stronger model first.",
|
|
418
506
|
"flow.recommend.primary": "Start with recommendation",
|
|
@@ -420,6 +508,10 @@ export const COPY: Record<Language, TextMap> = {
|
|
|
420
508
|
"flow.recommend.back": "Back",
|
|
421
509
|
"flow.recommend.skip": "Open Brain without a model",
|
|
422
510
|
"flow.recommend.hint": "If unsure, start with the recommendation.",
|
|
511
|
+
"flow.recommend.rank.best": "Best Experience",
|
|
512
|
+
"flow.recommend.rank.faster": "Faster",
|
|
513
|
+
"flow.recommend.rank.advanced": "Advanced",
|
|
514
|
+
"flow.recommend.rank.choice": "Choice {index}",
|
|
423
515
|
"flow.install.title": "Install the model and start.",
|
|
424
516
|
"flow.install.body": "This model becomes your Brain's local voice. Internet is needed only for download; execution and memory stay on this computer.",
|
|
425
517
|
"flow.install.wait": "Waiting for the model your Brain will use.",
|
|
@@ -457,7 +457,7 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
|
|
|
457
457
|
{(data) => <MemoryStatus data={data as Record<string, unknown>} />}
|
|
458
458
|
</DataPanel>
|
|
459
459
|
<DataPanel title="Recent sources" result={provenance.data}>
|
|
460
|
-
{(data) => <
|
|
460
|
+
{(data) => <SourceProvenanceList items={(data as Record<string, unknown>).items || data} />}
|
|
461
461
|
</DataPanel>
|
|
462
462
|
</div>
|
|
463
463
|
) : null}
|
|
@@ -527,6 +527,7 @@ function RetrievalStatus({ data }: { data: Record<string, unknown> }) {
|
|
|
527
527
|
|
|
528
528
|
function MemoryStatus({ data }: { data: Record<string, unknown> }) {
|
|
529
529
|
const usage = isRecord(data.usage) ? data.usage : {};
|
|
530
|
+
const sources = asArray<Record<string, unknown>>(data.sources || data.tiers);
|
|
530
531
|
return (
|
|
531
532
|
<div className="space-y-3">
|
|
532
533
|
<StatGrid stats={[
|
|
@@ -535,7 +536,7 @@ function MemoryStatus({ data }: { data: Record<string, unknown> }) {
|
|
|
535
536
|
{ label: "Bytes", value: usage.total_bytes ?? 0 },
|
|
536
537
|
{ label: "Health", value: data.health || "reported" },
|
|
537
538
|
]} />
|
|
538
|
-
<
|
|
539
|
+
<SourceProvenanceList items={sources} limit={6} />
|
|
539
540
|
</div>
|
|
540
541
|
);
|
|
541
542
|
}
|
|
@@ -667,15 +668,20 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
|
|
|
667
668
|
{mode === "basic" ? (
|
|
668
669
|
<KeyValueList data={{
|
|
669
670
|
connections: selected.degree,
|
|
670
|
-
source: selected.source || "not
|
|
671
|
+
source: selected.source || "Source not recorded",
|
|
672
|
+
source_type: sourceType(selected.raw),
|
|
673
|
+
created_at: sourceCreatedAt(selected.raw) || "Created time not recorded",
|
|
671
674
|
}} />
|
|
672
675
|
) : (
|
|
673
676
|
<StructuredView value={{
|
|
674
677
|
id: selected.id,
|
|
675
678
|
degree: selected.degree,
|
|
676
|
-
source: selected.source || "not
|
|
679
|
+
source: selected.source || "Source not recorded",
|
|
680
|
+
source_type: sourceType(selected.raw),
|
|
681
|
+
created_at: sourceCreatedAt(selected.raw) || "Created time not recorded",
|
|
677
682
|
}} />
|
|
678
683
|
)}
|
|
684
|
+
{selected.source ? <Button variant="outline" size="sm" onClick={() => void navigator.clipboard?.writeText(selected.source)}>Copy source</Button> : null}
|
|
679
685
|
</>
|
|
680
686
|
) : selectedGroup ? (
|
|
681
687
|
<div className="space-y-2">
|
|
@@ -779,12 +785,64 @@ function ProvenancePanel() {
|
|
|
779
785
|
{(data) => <StructuredView value={data} />}
|
|
780
786
|
</DataPanel>
|
|
781
787
|
<DataPanel title="Recent sources" result={provenance.data}>
|
|
782
|
-
{(data) => <
|
|
788
|
+
{(data) => <SourceProvenanceList items={(data as Record<string, unknown>).items || data} limit={14} />}
|
|
783
789
|
</DataPanel>
|
|
784
790
|
</div>
|
|
785
791
|
);
|
|
786
792
|
}
|
|
787
793
|
|
|
794
|
+
function SourceProvenanceList({ items, limit = 8 }: { items: unknown; limit?: number }) {
|
|
795
|
+
const rows = asArray<Record<string, unknown>>(items).slice(0, limit);
|
|
796
|
+
if (!rows.length) return <EmptyState title="No sources yet" detail="New memories will show their chat, manual, document, or import origin here." />;
|
|
797
|
+
return (
|
|
798
|
+
<div className="grid gap-2">
|
|
799
|
+
{rows.map((item, index) => {
|
|
800
|
+
const title = String(item.title || item.label || item.source_title || item.filename || item.path || item.source || `Source ${index + 1}`);
|
|
801
|
+
const path = String(item.path || item.source_path || item.source || item.conversation_id || "");
|
|
802
|
+
const type = sourceType(item);
|
|
803
|
+
const created = sourceCreatedAt(item);
|
|
804
|
+
return (
|
|
805
|
+
<div key={String(item.id || item.source_id || title)} className="rounded-lg border border-border bg-background/55 p-3">
|
|
806
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
807
|
+
<div>
|
|
808
|
+
<div className="font-medium">{title}</div>
|
|
809
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
810
|
+
{type} · {created || "Created time not recorded"}
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
<Badge variant="muted">{path ? "inspectable" : "missing provenance"}</Badge>
|
|
814
|
+
</div>
|
|
815
|
+
{path ? (
|
|
816
|
+
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
817
|
+
<span className="break-all">{path}</span>
|
|
818
|
+
<Button variant="outline" size="sm" onClick={() => void navigator.clipboard?.writeText(path)}>Copy source</Button>
|
|
819
|
+
</div>
|
|
820
|
+
) : (
|
|
821
|
+
<p className="mt-2 text-xs text-muted-foreground">This older memory did not record a source path or conversation. It remains searchable, but provenance is incomplete.</p>
|
|
822
|
+
)}
|
|
823
|
+
</div>
|
|
824
|
+
);
|
|
825
|
+
})}
|
|
826
|
+
</div>
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function sourceType(item: Record<string, unknown>) {
|
|
831
|
+
const metadata = isRecord(item.metadata) ? item.metadata : {};
|
|
832
|
+
const raw = String(item.source_type || item.type || item.kind || metadata.source_type || metadata.role || "").toLowerCase();
|
|
833
|
+
if (/chat|conversation|message/.test(raw)) return "chat";
|
|
834
|
+
if (/document|upload|file|pdf|markdown|text/.test(raw)) return "document";
|
|
835
|
+
if (/import|archive|restore/.test(raw)) return "import";
|
|
836
|
+
if (/manual|note/.test(raw)) return "manual";
|
|
837
|
+
return "source unknown";
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function sourceCreatedAt(item: Record<string, unknown>) {
|
|
841
|
+
const metadata = isRecord(item.metadata) ? item.metadata : {};
|
|
842
|
+
const value = item.created_at || item.timestamp || item.updated_at || metadata.created_at || metadata.timestamp;
|
|
843
|
+
return value ? String(value) : "";
|
|
844
|
+
}
|
|
845
|
+
|
|
788
846
|
function PortabilityPanel() {
|
|
789
847
|
const qc = useQueryClient();
|
|
790
848
|
const [artifact, setArtifact] = React.useState("");
|