ltcai 6.2.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 +29 -17
- package/docs/CHANGELOG.md +66 -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 +14 -10
- package/frontend/src/features/review/ReviewInbox.tsx +24 -9
- package/frontend/src/i18n.ts +208 -0
- package/frontend/src/pages/Brain.tsx +63 -5
- package/frontend/src/pages/Capture.tsx +139 -43
- 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 +48 -106
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/access_runtime.py +97 -0
- 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-Div5vMlq.css +2 -0
- package/static/app/assets/{index-D91Rz5--.js → index-t1jx1BR9.js} +2 -2
- package/static/app/assets/index-t1jx1BR9.js.map +1 -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
|
];
|
|
@@ -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,23 +75,25 @@ 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
|
-
<p className=
|
|
94
|
+
<p className={`mt-2 text-xs ${/fail|error|unavailable/i.test(feedback) ? "text-amber-300" : "text-emerald-300"}`}>
|
|
95
|
+
{feedback} - {t(language, "review.feedback.open")}
|
|
96
|
+
</p>
|
|
93
97
|
) : null}
|
|
94
98
|
</div>
|
|
95
99
|
);
|
|
@@ -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");
|
|
@@ -38,11 +41,23 @@ export function ReviewInbox() {
|
|
|
38
41
|
action === "unsnooze" ? () => latticeApi.unsnoozeReviewItem(item.id) :
|
|
39
42
|
() => latticeApi.runNowReviewItem(item.id);
|
|
40
43
|
const result = await call();
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
setRunFeedback((prev) => ({
|
|
46
|
+
...prev,
|
|
47
|
+
[item.id]: result.error || t(language, "review.action.failed", { action }),
|
|
48
|
+
}));
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
41
51
|
if (result.ok) {
|
|
42
52
|
if (action === "run_now") {
|
|
53
|
+
const payload = result.data.payload || {};
|
|
54
|
+
const provenance = result.data.provenance || {};
|
|
55
|
+
const runId = String(payload.last_run_id || provenance.run_id || "");
|
|
43
56
|
setRunFeedback((prev) => ({
|
|
44
57
|
...prev,
|
|
45
|
-
[item.id]:
|
|
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"),
|
|
46
61
|
}));
|
|
47
62
|
} else {
|
|
48
63
|
setRunFeedback((prev) => {
|
|
@@ -56,18 +71,18 @@ export function ReviewInbox() {
|
|
|
56
71
|
return result;
|
|
57
72
|
};
|
|
58
73
|
|
|
59
|
-
if (reviews.isLoading) return <LoadingPanel title="
|
|
74
|
+
if (reviews.isLoading) return <LoadingPanel title={t(language, "review.inbox.title")} />;
|
|
60
75
|
|
|
61
76
|
return (
|
|
62
77
|
<Card>
|
|
63
78
|
<CardHeader className="gap-3">
|
|
64
79
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
65
80
|
<div>
|
|
66
|
-
<CardTitle>
|
|
67
|
-
<CardDescription>
|
|
81
|
+
<CardTitle>{t(language, "review.inbox.title")}</CardTitle>
|
|
82
|
+
<CardDescription>{t(language, "review.inbox.description")}</CardDescription>
|
|
68
83
|
</div>
|
|
69
84
|
{reviews.data ? (
|
|
70
|
-
<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>
|
|
71
86
|
) : null}
|
|
72
87
|
</div>
|
|
73
88
|
<div className="grid gap-2">
|
|
@@ -86,13 +101,13 @@ export function ReviewInbox() {
|
|
|
86
101
|
<CardContent>
|
|
87
102
|
{reviews.isError || (reviews.data && !reviews.data.ok) ? (
|
|
88
103
|
<EmptyState
|
|
89
|
-
title="
|
|
90
|
-
detail={reviews.data?.error || "
|
|
104
|
+
title={t(language, "review.inbox.loadError")}
|
|
105
|
+
detail={reviews.data?.error || t(language, "review.inbox.unavailable")}
|
|
91
106
|
/>
|
|
92
107
|
) : !items.length ? (
|
|
93
108
|
<EmptyState
|
|
94
|
-
title="
|
|
95
|
-
detail={statusFilter === "snoozed" ? "
|
|
109
|
+
title={t(language, "review.inbox.empty")}
|
|
110
|
+
detail={statusFilter === "snoozed" ? t(language, "review.inbox.emptySnoozed") : t(language, "review.inbox.emptyPending")}
|
|
96
111
|
/>
|
|
97
112
|
) : (
|
|
98
113
|
<div className="grid gap-3">
|