mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
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:
1 parent
4cf66eb379
commit
139b454efd
10 files changed
+170
-3
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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,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."
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "允许模型在响应前进行推理。更高级别使用更多令牌,但在复杂任务上产生更好的结果。",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user