feat(ui): dreaming config section and context usage badge

Phase 11 — web UI components:
- DreamingConfig TS interface + nested block in MemorySection (4 controls)
- Context usage badge in ChatTopBar: used/max/percent with color ramp
- Badge tooltip surfaces last_compaction_at from session.metadata
- i18n keys in en/vi/zh: configSections.dreaming.*, chat.contextUsage.*
- Hidden on mobile via hidden sm:flex (follow-up: compact variant)
This commit is contained in:
viettranx committed 2026-04-10 13:32:07 +07:00
1 parent 4cf66eb379
commit 139b454efd
10 files changed
+170 -3

No files matched your search

+48 -1
View File
@@ -1,9 +1,11 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Loader2, Bot, Users, PanelRightOpen, PanelRightClose } from "lucide-react";
import { useHttp } from "@/hooks/use-ws";
import { useAuthStore } from "@/stores/use-auth-store";
import type { RunActivity, ActiveTeamTask } from "@/types/chat";
import type { AgentData } from "@/types/agent";
import type { SessionInfo } from "@/types/session";
interface ChatTopBarProps {
agentId: string;
@@ -13,6 +15,8 @@ interface ChatTopBarProps {
teamTasks: ActiveTeamTask[];
onToggleTaskPanel?: () => void;
taskPanelOpen?: boolean;
/** Current session — when provided, the bar renders a context-usage badge. */
session?: SessionInfo | null;
}
const phaseLabels: Record<RunActivity["phase"], string> = {
@@ -24,8 +28,9 @@ const phaseLabels: Record<RunActivity["phase"], string> = {
leader_processing: "Processing team results…",
};
export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks, onToggleTaskPanel, taskPanelOpen }: ChatTopBarProps) {
export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks, onToggleTaskPanel, taskPanelOpen, session }: ChatTopBarProps) {
const http = useHttp();
const { t } = useTranslation("chat");
const connected = useAuthStore((s) => s.connected);
const [agent, setAgent] = useState<{ name: string; emoji?: string } | null>(null);
@@ -51,6 +56,31 @@ export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks, on
const emoji = agent?.emoji;
const PanelIcon = taskPanelOpen ? PanelRightClose : PanelRightOpen;
// Context-usage badge: only renders when the caller passes a session with
// both estimatedTokens (Phase 4 ContextStage output) and contextWindow.
// `percent` drives the color ramp so operators spot near-limit sessions.
const usage = (() => {
if (!session || !session.contextWindow || session.contextWindow <= 0) {
return null;
}
const used = session.estimatedTokens ?? 0;
const max = session.contextWindow;
const percent = Math.min(100, Math.round((used / max) * 100));
const color =
percent >= 90 ? "text-destructive" : percent >= 75 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground";
return { used, max, percent, color };
})();
// Last compaction timestamp ships in sessions.metadata JSONB (Phase 5 follow-up,
// keyed "last_compaction_at"). Parsed lazily so bad data doesn't crash the bar.
const lastCompaction = (() => {
const raw = session?.metadata?.last_compaction_at;
if (!raw) return null;
const d = new Date(raw);
if (isNaN(d.getTime())) return null;
return d;
})();
return (
<div className="flex items-center justify-between border-b px-4 py-1.5">
<div className="flex items-center gap-2">
@@ -63,6 +93,23 @@ export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks, on
</div>
<div className="flex items-center gap-2">
{usage && (
<div
className={`hidden sm:flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] ${usage.color}`}
title={t("contextUsage.tooltip", {
used: usage.used.toLocaleString(),
max: usage.max.toLocaleString(),
percent: usage.percent,
compactions: session?.compactionCount ?? 0,
lastCompact: lastCompaction ? lastCompaction.toLocaleString() : t("contextUsage.never"),
})}
>
<span className="font-mono">
{usage.used.toLocaleString()}/{usage.max.toLocaleString()}
</span>
<span className="opacity-70">({usage.percent}%)</span>
</div>
)}
{isRunning ? (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span>{activity ? phaseLabels[activity.phase] : "Running…"}</span>
+12
View File
@@ -826,6 +826,18 @@
"textWeight": "Text Weight",
"textWeightTip": "Weight for keyword similarity in hybrid search."
},
"dreaming": {
"title": "Dreaming (Memory Consolidation)",
"description": "Background worker that promotes session summaries into long-term memory. Tune when the agent is too chatty or not consolidating often enough.",
"enabled": "Enabled",
"enabledTip": "Disable if you want the agent to rely only on raw session summaries without long-term consolidation.",
"threshold": "Threshold",
"thresholdTip": "Minimum unpromoted summaries before a consolidation run fires. Default 5.",
"debounceMs": "Debounce (ms)",
"debounceMsTip": "Minimum interval between runs per agent/user. Default 600000 (10 minutes).",
"verboseLog": "Verbose Logging",
"verboseLogTip": "Emit debounce and below-threshold skips at info level for operator debugging."
},
"thinking": {
"title": "Extended Thinking",
"description": "Allow the model to reason before responding. Higher levels use more tokens but produce better results on complex tasks.",
+4
View File
@@ -4,6 +4,10 @@
"newChat": "New Chat",
"openSessions": "Open sessions",
"readOnly": "Read-only — this session belongs to another user",
"contextUsage": {
"tooltip": "{{used}} / {{max}} tokens ({{percent}}%) · {{compactions}} compactions · last: {{lastCompact}}",
"never": "never"
},
"empty": {
"title": "Start a conversation",
"description": "Send a message to begin chatting with the agent."
+12
View File
@@ -783,6 +783,18 @@
"textWeight": "Trọng số văn bản",
"textWeightTip": "Trọng số từ khóa trong tìm kiếm hỗn hợp."
},
"dreaming": {
"title": "Dreaming (hợp nhất bộ nhớ)",
"description": "Worker nền ghi các tóm tắt phiên vào bộ nhớ dài hạn. Tinh chỉnh nếu agent hợp nhất quá thường xuyên hoặc quá ít.",
"enabled": "Đã bật",
"enabledTip": "Tắt nếu bạn chỉ muốn dùng tóm tắt phiên thô, không cần hợp nhất dài hạn.",
"threshold": "Ngưỡng",
"thresholdTip": "Số tóm tắt chưa promote tối thiểu trước khi chạy hợp nhất. Mặc định 5.",
"debounceMs": "Debounce (ms)",
"debounceMsTip": "Khoảng thời gian tối thiểu giữa hai lần chạy cho mỗi agent/user. Mặc định 600000 (10 phút).",
"verboseLog": "Log chi tiết",
"verboseLogTip": "Ghi các lần skip (debounce, below-threshold) ở mức info để debug."
},
"thinking": {
"title": "Suy nghĩ mở rộng",
"description": "Cho phép model suy luận trước khi phản hồi. Cấp độ cao hơn dùng nhiều token hơn nhưng cho kết quả tốt hơn với tác vụ phức tạp.",
+4
View File
@@ -1,4 +1,8 @@
{
"contextUsage": {
"tooltip": "{{used}} / {{max}} token ({{percent}}%) · {{compactions}} lần compact · gần nhất: {{lastCompact}}",
"never": "chưa có"
},
"empty": {
"description": "Gửi tin nhắn để bắt đầu trò chuyện với agent.",
"title": "Bắt đầu cuộc trò chuyện"
+12
View File
@@ -783,6 +783,18 @@
"textWeight": "文本权重",
"textWeightTip": "混合搜索中关键词相似度的权重。"
},
"dreaming": {
"title": "Dreaming(记忆整合)",
"description": "后台工作器将会话摘要提升为长期记忆。根据Agent整合频率调整参数。",
"enabled": "已启用",
"enabledTip": "关闭后Agent将仅使用原始会话摘要,不进行长期整合。",
"threshold": "阈值",
"thresholdTip": "触发整合运行所需的最少未提升摘要数。默认5。",
"debounceMs": "防抖间隔 (ms)",
"debounceMsTip": "每个Agent/用户两次运行之间的最小间隔。默认600000(10分钟)。",
"verboseLog": "详细日志",
"verboseLogTip": "在info级别输出防抖和低于阈值跳过,便于运维调试。"
},
"thinking": {
"title": "扩展思考",
"description": "允许模型在响应前进行推理。更高级别使用更多令牌,但在复杂任务上产生更好的结果。",
+4
View File
@@ -1,4 +1,8 @@
{
"contextUsage": {
"tooltip": "{{used}} / {{max}} tokens ({{percent}}%) · 压缩 {{compactions}} 次 · 最近:{{lastCompact}}",
"never": "无"
},
"empty": {
"description": "发送消息开始与Agent对话。",
"title": "开始对话"
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { MemoryConfig } from "@/types/agent";
import type { MemoryConfig, DreamingConfig } from "@/types/agent";
import { InfoLabel, numOrUndef } from "./config-section";
interface MemorySectionProps {
@@ -12,6 +12,10 @@ interface MemorySectionProps {
export function MemorySection({ value, onChange }: MemorySectionProps) {
const { t } = useTranslation("agents");
const s = "configSections.memory";
const ds = "configSections.dreaming";
const dreaming: DreamingConfig = value.dreaming ?? {};
const setDreaming = (patch: Partial<DreamingConfig>) =>
onChange({ ...value, dreaming: { ...dreaming, ...patch } });
return (
<section className="space-y-3">
<div>
@@ -95,6 +99,51 @@ export function MemorySection({ value, onChange }: MemorySectionProps) {
/>
</div>
</div>
{/* Phase 8 — Dreaming worker per-agent overrides.
Nested inside memory since dreaming is memory consolidation.
All fields optional; nil = use backend defaults. */}
<div className="rounded-md border border-dashed p-3 space-y-4">
<div>
<h4 className="text-sm font-medium">{t(`${ds}.title`)}</h4>
<p className="text-xs text-muted-foreground">{t(`${ds}.description`)}</p>
</div>
<div className="flex items-center gap-2">
<Switch
checked={dreaming.enabled ?? true}
onCheckedChange={(v) => setDreaming({ enabled: v })}
/>
<InfoLabel tip={t(`${ds}.enabledTip`)}>{t(`${ds}.enabled`)}</InfoLabel>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<InfoLabel tip={t(`${ds}.thresholdTip`)}>{t(`${ds}.threshold`)}</InfoLabel>
<Input
type="number"
placeholder="5"
value={dreaming.threshold ?? ""}
onChange={(e) => setDreaming({ threshold: numOrUndef(e.target.value) })}
className="text-base md:text-sm"
/>
</div>
<div className="space-y-2">
<InfoLabel tip={t(`${ds}.debounceMsTip`)}>{t(`${ds}.debounceMs`)}</InfoLabel>
<Input
type="number"
placeholder="600000"
value={dreaming.debounce_ms ?? ""}
onChange={(e) => setDreaming({ debounce_ms: numOrUndef(e.target.value) })}
className="text-base md:text-sm"
/>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={dreaming.verbose_log ?? false}
onCheckedChange={(v) => setDreaming({ verbose_log: v })}
/>
<InfoLabel tip={t(`${ds}.verboseLogTip`)}>{t(`${ds}.verboseLog`)}</InfoLabel>
</div>
</div>
</div>
</section>
);
+10 -1
View File
@@ -234,7 +234,16 @@ export function ChatPage() {
)}
<div className="shrink-0">
<ChatTopBar agentId={agentId} isRunning={isRunning} isBusy={isBusy} activity={activity} teamTasks={teamTasks} onToggleTaskPanel={() => setTaskPanelOpen((v) => !v)} taskPanelOpen={taskPanelOpen} />
<ChatTopBar
agentId={agentId}
isRunning={isRunning}
isBusy={isBusy}
activity={activity}
teamTasks={teamTasks}
onToggleTaskPanel={() => setTaskPanelOpen((v) => !v)}
taskPanelOpen={taskPanelOpen}
session={sessions.find((s) => s.key === sessionKey) ?? null}
/>
</div>
{sendError && (
+14
View File
@@ -67,6 +67,20 @@ export interface MemoryConfig {
vector_weight?: number;
text_weight?: number;
min_score?: number;
dreaming?: DreamingConfig | null;
}
/**
* DreamingConfig mirrors Go internal/config.DreamingConfig — per-agent
* overrides for the consolidation dreaming worker. Pointer fields stay
* undefined when the operator hasn't set them so partial overrides merge
* cleanly with backend defaults.
*/
export interface DreamingConfig {
enabled?: boolean;
debounce_ms?: number;
threshold?: number;
verbose_log?: boolean;
}
export interface WorkspaceSharingConfig {