feat: add Russian localization and independent STT languages
This commit is contained in:
+77
-75
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
import { translator, countLabel, type Language } from "@/src/core";
|
||||
import { useI18n } from "@/components/i18n";
|
||||
|
||||
|
||||
/**
|
||||
* The home knowledge-graph view (W4; vision §1 "Rendering (DECIDED)").
|
||||
@@ -276,7 +279,8 @@ function SettingSlider({
|
||||
}
|
||||
|
||||
/** Compact token display: 45231 → "45.2k"; absent → "—". */
|
||||
function fmtTokens(n: number | undefined): string {
|
||||
function fmtTokens(n: number | undefined, language: Language = "en"): string {
|
||||
if (language === "ru" && n !== undefined) return new Intl.NumberFormat("ru-RU", { notation: "compact", maximumFractionDigits: 1 }).format(n);
|
||||
if (n === undefined) return "—";
|
||||
if (n < 1000) return String(Math.round(n));
|
||||
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
||||
@@ -284,29 +288,30 @@ function fmtTokens(n: number | undefined): string {
|
||||
}
|
||||
|
||||
/** Relative time for run rows: "now", "12m", "3h", "2d". */
|
||||
function timeAgo(startedAt: number): string {
|
||||
function timeAgo(startedAt: number, language: Language = "en"): string {
|
||||
const t = translator(language);
|
||||
const mins = Math.round((Date.now() - startedAt) / 60_000);
|
||||
if (mins < 1) return "now";
|
||||
if (mins < 60) return `${mins}m`;
|
||||
if (mins < 1) return t("now");
|
||||
if (mins < 60) return t("{v0}m", { v0: mins });
|
||||
const hours = Math.round(mins / 60);
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${Math.round(hours / 24)}d`;
|
||||
if (hours < 24) return t("{v0}h", { v0: hours });
|
||||
return t("{v0}d", { v0: Math.round(hours / 24) });
|
||||
}
|
||||
|
||||
/** Run/stage cost at 3 decimals (pipeline pennies are real money at this
|
||||
* scale); headline totals use 2. Null (unpriced model) renders "—". */
|
||||
function fmtRunCost(dollars: number | null, providers: readonly string[] = []): string {
|
||||
return planCostLabel(dollars, providers, (value) => `$${value.toFixed(3)}`);
|
||||
function fmtRunCost(dollars: number | null, providers: readonly string[] = [], language: Language = "en"): string {
|
||||
return planCostLabel(dollars, providers, (value) => language === "ru" ? `${value.toLocaleString("ru-RU", { minimumFractionDigits: 3, maximumFractionDigits: 3 })} $` : `$${value.toFixed(3)}`);
|
||||
}
|
||||
function graphProviders(value: { subscriptionRuns?: number; subscriptionProviders?: string[] }): string[] {
|
||||
return value.subscriptionProviders ?? ((value.subscriptionRuns ?? 0) > 0 ? ["ChatGPT"] : []);
|
||||
}
|
||||
function graphUsageLabel(costs: Pick<GraphCostSummary, "stages">): string {
|
||||
function graphUsageLabel(costs: Pick<GraphCostSummary, "stages">, language: Language = "en"): string {
|
||||
const stages = [costs.stages.extraction, costs.stages.audit];
|
||||
const providers = [...new Set(stages.flatMap(graphProviders))];
|
||||
const priced = stages.some((stage) => stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) > 0);
|
||||
const dollars = displayCostTotals(costs).totalDollars;
|
||||
return planCostLabel(priced ? dollars : null, providers, (value) => `$${value.toFixed(2)}`);
|
||||
return planCostLabel(priced ? dollars : null, providers, (value) => language === "ru" ? `${value.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} $` : `$${value.toFixed(2)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,6 +345,7 @@ export function displayCostTotals(costs: Pick<GraphCostSummary, "stages">): {
|
||||
/** Exported for the chains home (graph rendering off, owner
|
||||
* 2026-10-06): the same bare cumulative-dollars pill + panel. */
|
||||
export function GraphCostControl({ costs }: { costs: GraphCostSummary }) {
|
||||
const { t, language } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
// Click anywhere outside (or Escape) closes the panel — the root
|
||||
// wraps button + panel, so the toggle itself keeps working.
|
||||
@@ -349,7 +355,7 @@ export function GraphCostControl({ costs }: { costs: GraphCostSummary }) {
|
||||
<div ref={rootRef} className="relative">
|
||||
<Hint
|
||||
// Trimmed (owner, subtlety round 2026-09-25).
|
||||
label="Graph costs — click for details"
|
||||
label={t("Graph costs — click for details")}
|
||||
side="bottom"
|
||||
>
|
||||
<button
|
||||
@@ -360,7 +366,7 @@ export function GraphCostControl({ costs }: { costs: GraphCostSummary }) {
|
||||
open ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{graphUsageLabel(costs)}
|
||||
{graphUsageLabel(costs, language)}
|
||||
</button>
|
||||
</Hint>
|
||||
{open && (
|
||||
@@ -383,9 +389,10 @@ export function GraphCostControl({ costs }: { costs: GraphCostSummary }) {
|
||||
* naming-kind runs never reach the list.
|
||||
*/
|
||||
function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
||||
const { t, language } = useI18n();
|
||||
const stageRows: [string, GraphCostSummary["stages"]["extraction"]][] = [
|
||||
["Extraction", costs.stages.extraction],
|
||||
["Audit", costs.stages.audit],
|
||||
[t("Extraction"), costs.stages.extraction],
|
||||
[t("Audit"), costs.stages.audit],
|
||||
];
|
||||
// The displayed figures come from displayCostTotals (naming excluded —
|
||||
// the pill and this panel must agree).
|
||||
@@ -394,12 +401,11 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
||||
<div className="w-80 max-w-[calc(100vw-1.5rem)] rounded-xl border bg-card/95 p-3 text-xs shadow-lg backdrop-blur">
|
||||
<div className="mb-2 flex items-baseline justify-between">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Graph pipeline costs
|
||||
</span>
|
||||
{t("Graph pipeline costs")}</span>
|
||||
<span className="text-foreground tabular-nums">
|
||||
{graphUsageLabel(costs)}
|
||||
{graphUsageLabel(costs, language)}
|
||||
{unpricedRuns > 0 && (
|
||||
<span className="text-muted-foreground"> · {unpricedRuns} unpriced</span>
|
||||
<span className="text-muted-foreground"> · {unpricedRuns}{" "}{t("unpriced")}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -408,14 +414,13 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
||||
<div key={label} className="flex items-baseline justify-between gap-2 text-[11px]">
|
||||
<span className="shrink-0 text-muted-foreground">{label}</span>
|
||||
<span className="min-w-0 text-right tabular-nums">
|
||||
{fmtRunCost(stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) <= 0 ? null : stage.dollars, graphProviders(stage))}
|
||||
{fmtRunCost(stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) <= 0 ? null : stage.dollars, graphProviders(stage), language)}
|
||||
{stage.unpricedRuns > 0 && (stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) > 0 || (stage.subscriptionRuns ?? 0) > 0) && (
|
||||
<span className="text-muted-foreground"> +{stage.unpricedRuns} unpriced</span>
|
||||
<span className="text-muted-foreground"> +{stage.unpricedRuns}{" "}{t("unpriced")}</span>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
· {fmtTokens(stage.tokens.inputTokens)} in / {fmtTokens(stage.tokens.outputTokens)} out
|
||||
</span>
|
||||
· {fmtTokens(stage.tokens.inputTokens, language)}{" "}{t("in /")}{" "}{fmtTokens(stage.tokens.outputTokens, language)}{" "}{t("out")}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -424,16 +429,16 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
||||
{costs.runs.map((run) => (
|
||||
<div key={run.startedAt} className="flex items-baseline justify-between gap-2 text-[11px]">
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
||||
<span className="text-foreground/70">{timeAgo(run.startedAt)}</span>{" "}
|
||||
<span className="text-foreground/70">{timeAgo(run.startedAt, language)}</span>{" "}
|
||||
{run.title}
|
||||
{run.merged && (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
+{run.merged.nodes}n
|
||||
+{language === "ru" ? countLabel(language, run.merged.nodes, "node", "nodes") : `${run.merged.nodes}n`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="shrink-0 tabular-nums">{fmtRunCost(run.dollars, graphProviders(run))}{run.dollars === null && run.subscriptionRuns === 1 ? " +1 unpriced" : ""}</span>
|
||||
<span className="shrink-0 tabular-nums">{fmtRunCost(run.dollars, graphProviders(run), language)}{run.dollars === null && run.subscriptionRuns === 1 ? t(" +1 unpriced") : ""}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -454,6 +459,7 @@ function GraphSettingsControl({
|
||||
settings: GraphRenderSettings;
|
||||
changeSettings: (update: (cur: RenderSettings) => RenderSettings) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
// Click anywhere outside (or Escape) closes the panel (2026-09-17) —
|
||||
// the root wraps button + panel, so the toggle itself keeps working.
|
||||
@@ -465,7 +471,7 @@ function GraphSettingsControl({
|
||||
<div ref={rootRef} className="relative">
|
||||
<Hint
|
||||
// User language (tooltip wave) — "rendering" is internal.
|
||||
label="Graph display settings"
|
||||
label={t("Graph display settings")}
|
||||
side="bottom"
|
||||
>
|
||||
<button
|
||||
@@ -498,7 +504,7 @@ function GraphSettingsControl({
|
||||
number — src/core/render-settings.ts). Physics keeps the
|
||||
fine-grained log sliders (step 5). */}
|
||||
<SettingSlider
|
||||
label="Node size"
|
||||
label={t("Node size")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
@@ -508,7 +514,7 @@ function GraphSettingsControl({
|
||||
changeSettings((cur) => ({ ...cur, nodeSize: fromLadderPosition(p, NODE_SIZE_LADDER) }))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Edge width"
|
||||
label={t("Edge width")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
@@ -518,7 +524,7 @@ function GraphSettingsControl({
|
||||
changeSettings((cur) => ({ ...cur, edgeWidth: fromLadderPosition(p, EDGE_WIDTH_LADDER) }))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Arrowhead size"
|
||||
label={t("Arrowhead size")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
@@ -528,7 +534,7 @@ function GraphSettingsControl({
|
||||
changeSettings((cur) => ({ ...cur, arrowLength: fromLadderPosition(p, ARROW_LENGTH_LADDER) }))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Label size"
|
||||
label={t("Label size")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
@@ -539,7 +545,7 @@ function GraphSettingsControl({
|
||||
/>
|
||||
{/* Display is absolute (ratio / 0.5): "1×" is the default, dead-center (owner, 2026-10-07). */}
|
||||
<SettingSlider
|
||||
label="Labels until zoom"
|
||||
label={t("Labels until zoom")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
@@ -557,10 +563,9 @@ function GraphSettingsControl({
|
||||
the paint. */}
|
||||
<div className="mt-1.5 flex flex-col gap-1.5 border-t pt-1.5">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Regions
|
||||
</span>
|
||||
{t("Regions")}</span>
|
||||
<label className="flex cursor-pointer items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>Background colors</span>
|
||||
<span>{t("Background colors")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.regions.enabled}
|
||||
@@ -573,7 +578,7 @@ function GraphSettingsControl({
|
||||
/>
|
||||
</label>
|
||||
<SettingSlider
|
||||
label="Dilation"
|
||||
label={t("Dilation")}
|
||||
disabled={!regionTuning.enabled}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -587,7 +592,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Blur"
|
||||
label={t("Blur")}
|
||||
disabled={!regionTuning.enabled}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -602,7 +607,7 @@ function GraphSettingsControl({
|
||||
}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Edge alpha"
|
||||
label={t("Edge alpha")}
|
||||
disabled={!regionTuning.enabled}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -618,7 +623,7 @@ function GraphSettingsControl({
|
||||
<label
|
||||
className={`flex items-center justify-between gap-2 text-[11px] text-muted-foreground${!regionTuning.enabled ? " opacity-40" : ""}`}
|
||||
>
|
||||
<span>Bitmap resolution</span>
|
||||
<span>{t("Bitmap resolution")}</span>
|
||||
<select
|
||||
disabled={!regionTuning.enabled}
|
||||
className="rounded-md border bg-background px-1.5 py-0.5 text-xs tabular-nums text-foreground"
|
||||
@@ -643,7 +648,7 @@ function GraphSettingsControl({
|
||||
their own clusterLabels toggle. Size is screen px
|
||||
(constant at any zoom, unlike node labels). */}
|
||||
<label className="flex cursor-pointer items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>Cluster labels</span>
|
||||
<span>{t("Cluster labels")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.clusterLabels}
|
||||
@@ -652,7 +657,7 @@ function GraphSettingsControl({
|
||||
/>
|
||||
</label>
|
||||
<SettingSlider
|
||||
label="Cluster label size"
|
||||
label={t("Cluster label size")}
|
||||
disabled={!settings.clusterLabels}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -678,10 +683,9 @@ function GraphSettingsControl({
|
||||
anchor + its gray-out-able controls last. */}
|
||||
<div className="mt-1.5 flex flex-col gap-1.5 border-t pt-1.5">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Physics
|
||||
</span>
|
||||
{t("Physics")}</span>
|
||||
<SettingSlider
|
||||
label="Node repulsion"
|
||||
label={t("Node repulsion")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
@@ -694,7 +698,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Edge attraction"
|
||||
label={t("Edge attraction")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
@@ -707,7 +711,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Edge length"
|
||||
label={t("Edge length")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
@@ -720,7 +724,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Central pull"
|
||||
label={t("Central pull")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
@@ -733,12 +737,12 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Cluster pull"
|
||||
label={t("Cluster pull")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
value={toOffSliderPosition(physicsTuning.clusterPull, CLUSTER_PULL_RANGE.min, CLUSTER_PULL_RANGE.max)}
|
||||
display={physicsTuning.clusterPull <= 0 ? "off" : sizeDisplay(physicsTuning.clusterPull)}
|
||||
display={physicsTuning.clusterPull <= 0 ? t("off") : sizeDisplay(physicsTuning.clusterPull)}
|
||||
onChange={(p) =>
|
||||
changeSettings((cur) => ({
|
||||
...cur,
|
||||
@@ -746,7 +750,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Friction"
|
||||
label={t("Friction")}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
@@ -763,8 +767,7 @@ function GraphSettingsControl({
|
||||
controls at the very bottom — the grayed group reads
|
||||
as a self-contained sub-section. */}
|
||||
<label className="flex items-center justify-between gap-2 text-xs">
|
||||
Depth anchor
|
||||
<select
|
||||
{t("Depth anchor")}<select
|
||||
className="rounded border bg-background px-1 py-0.5 text-xs"
|
||||
value={physicsTuning.depthMode}
|
||||
onChange={(e) =>
|
||||
@@ -774,13 +777,13 @@ function GraphSettingsControl({
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="vertical">Vertical (up)</option>
|
||||
<option value="radial">Radial (outward)</option>
|
||||
<option value="off">{t("Off")}</option>
|
||||
<option value="vertical">{t("Vertical (up)")}</option>
|
||||
<option value="radial">{t("Radial (outward)")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<SettingSlider
|
||||
label="Depth strength"
|
||||
label={t("Depth strength")}
|
||||
disabled={physicsTuning.depthMode === "off"}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -794,7 +797,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Depth spacing"
|
||||
label={t("Depth spacing")}
|
||||
disabled={physicsTuning.depthMode === "off"}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -808,7 +811,7 @@ function GraphSettingsControl({
|
||||
}))}
|
||||
/>
|
||||
<SettingSlider
|
||||
label="Base ring radius"
|
||||
label={t("Base ring radius")}
|
||||
disabled={physicsTuning.depthMode !== "radial"}
|
||||
min={0}
|
||||
max={100}
|
||||
@@ -866,13 +869,14 @@ export function GraphChromeRow({
|
||||
* 2026-09-29). */
|
||||
onOpenChains: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<>
|
||||
{!isEmpty && (
|
||||
<div className="graph-chrome-fade-in w-56 min-w-0 shrink rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
||||
<Hint
|
||||
// ⌘F focuses node search (surface-scoped registry action).
|
||||
label="Search nodes"
|
||||
label={t("Search nodes")}
|
||||
action="find"
|
||||
side="bottom"
|
||||
>
|
||||
@@ -882,15 +886,15 @@ export function GraphChromeRow({
|
||||
ref={searchInputRef}
|
||||
value={query}
|
||||
onChange={(e) => onQuery(e.target.value)}
|
||||
placeholder="Search nodes…"
|
||||
aria-label="Search nodes"
|
||||
placeholder={t("Search nodes…")}
|
||||
aria-label={t("Search nodes")}
|
||||
spellCheck={false}
|
||||
className="h-7 w-full rounded-lg border bg-background pl-8 text-xs outline-none placeholder:text-muted-foreground focus:border-ring"
|
||||
/>
|
||||
</div>
|
||||
</Hint>
|
||||
{noMatch && (
|
||||
<div className="px-1.5 pt-1 text-[11px] text-muted-foreground">No matching nodes.</div>
|
||||
<div className="px-1.5 pt-1 text-[11px] text-muted-foreground">{t("No matching nodes.")}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -898,7 +902,7 @@ export function GraphChromeRow({
|
||||
container (36px band), between the search and Import. */}
|
||||
{!isEmpty && (
|
||||
<div className="graph-chrome-fade-in shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
||||
<Hint label="Every reasoning chain" side="bottom">
|
||||
<Hint label={t("Every reasoning chain")} side="bottom">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenChains}
|
||||
@@ -906,8 +910,7 @@ export function GraphChromeRow({
|
||||
className="flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-xs text-muted-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
<ListTreeIcon className="size-3" />
|
||||
Chains
|
||||
</button>
|
||||
{t("Chains")}</button>
|
||||
</Hint>
|
||||
</div>
|
||||
)}
|
||||
@@ -929,7 +932,7 @@ export function GraphChromeRow({
|
||||
// The one button whose action isn't obvious from its label:
|
||||
// the missing fact is where imports go (owner, subtlety
|
||||
// round 2026-09-25).
|
||||
label="Import notes into the graph"
|
||||
label={t("Import notes into the graph")}
|
||||
side="bottom"
|
||||
>
|
||||
<button
|
||||
@@ -940,7 +943,7 @@ export function GraphChromeRow({
|
||||
className="flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-xs text-muted-foreground transition-colors hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
<UploadIcon className="size-3" />
|
||||
{importing ? "Importing…" : "Import notes…"}
|
||||
{importing ? t("Importing…") : t("Import notes…")}
|
||||
</button>
|
||||
</Hint>
|
||||
</div>
|
||||
@@ -974,6 +977,7 @@ export function GraphView({
|
||||
onPracticeSelectionChange?: (nodes: { id: string; name: string; statement: string }[]) => void;
|
||||
onChainSelectClose?: () => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -1498,10 +1502,9 @@ export function GraphView({
|
||||
<div className="flex h-full w-full items-center justify-center px-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center text-muted-foreground">
|
||||
<WaypointsIcon className="size-10 opacity-40" strokeWidth={1.25} />
|
||||
<div className="text-lg font-medium text-foreground">Knowledge graph</div>
|
||||
<div className="text-lg font-medium text-foreground">{t("Knowledge graph")}</div>
|
||||
<p className="max-w-sm text-sm">
|
||||
Start a session or import notes to build your graph.
|
||||
</p>
|
||||
{t("Start a session or import notes to build your graph.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1541,8 +1544,7 @@ export function GraphView({
|
||||
}}
|
||||
className="flex items-center rounded-lg px-2.5 py-1.5 text-left text-muted-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{t("Delete")}</button>
|
||||
</MenuShell>
|
||||
)}
|
||||
|
||||
@@ -1630,9 +1632,9 @@ export function GraphView({
|
||||
className="flex items-center gap-2 rounded-full border bg-card/85 px-3 py-1.5 text-xs text-muted-foreground shadow-lg backdrop-blur"
|
||||
>
|
||||
<Loader2Icon className="size-3.5 shrink-0 animate-spin" />
|
||||
<span className="capitalize">{run.phase}</span>
|
||||
<span className="capitalize">{t(run.phase)}</span>
|
||||
<span className="max-w-48 truncate text-foreground">{run.title}</span>
|
||||
<span className="tabular-nums">{Math.max(0, Math.round((Date.now() - run.startedAt) / 1000))}s</span>
|
||||
<span className="tabular-nums">{t("{v0}s", { v0: Math.max(0, Math.round((Date.now() - run.startedAt) / 1000)) })}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1652,7 +1654,7 @@ export function GraphView({
|
||||
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{selected.node.statement}</div>
|
||||
{selected.sourceIds.length > 0 ? (
|
||||
<div className="mt-3 flex flex-col gap-1">
|
||||
<div className="text-xs text-muted-foreground">Sessions</div>
|
||||
<div className="text-xs text-muted-foreground">{t("Sessions")}</div>
|
||||
{selected.sourceIds.map((sourceId) => {
|
||||
// An imported-notes file: a STATIC entry — the path,
|
||||
// plain text, never a link (no session to open).
|
||||
@@ -1681,7 +1683,7 @@ export function GraphView({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 text-xs text-muted-foreground">No sessions recorded.</div>
|
||||
<div className="mt-3 text-xs text-muted-foreground">{t("No sessions recorded.")}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user