diff --git a/frontend/app/app/assistant.tsx b/frontend/app/app/assistant.tsx index e71e5f4..0f70b42 100644 --- a/frontend/app/app/assistant.tsx +++ b/frontend/app/app/assistant.tsx @@ -4,7 +4,6 @@ import type { ExportedMessageRepository } from "@assistant-ui/core"; import { AssistantRuntimeProvider, useAui, - useAuiState, } from "@assistant-ui/react"; import { AssistantChatTransport, @@ -195,28 +194,6 @@ function getDraftKey(activeSessionId: string | null): string { return activeSessionId; } -// 用户在对话里表达「我要开始/进行模型训练(这一轮迭代)」时命中的关键短语。 -// 要求动词或专属触发词必须存在;避免 "什么是模型训练" 之类无意义匹配。 -const APPLY_INTENT_PATTERNS: RegExp[] = [ - // 触发短语:开始,xxx / 开始, xxx - /(?:^|\s|[,,。.!?])开始\s*[,,]\s*\S+/, - // 单纯训练动作:进行/开始/启动/开启/执行/发起/做 + (模型) + 训练 - /(?:进行|开始|启动|开启|执行|发起|做)\s*(?:模型)?\s*训练/i, - // 训练 + 目标集合 - /训练[\s\S]{0,60}?(?:目标集合|需求集合|specific[\s_-]?test|集合)/i, - // 「(模型)训练,目标是 X」「训练,基模 X」「训练 ... csv」之类自然表达 - /(?:^|[\s,,。.!?])(?:模型)?训练[\s,,][\s\S]{0,80}?(?:目标|基模|specific[\s_-]?test|\.csv)/i, - // 跟着 program.md 的旧表达保留 - /(?:应用|加载|载入|启用|装载|使用|跑|按照?|根据|用)\s*\S*program\.?md/i, - /program\.?md[\s\S]{0,120}?(?:训练|应用|启用|跑起来|跑一下)/i, - /apply\s+program(?:\.md)?/i, - /load\s+program(?:\.md)?/i, - /(?:start|begin|run)\s+(?:the\s+)?training/i, -]; - -function detectApplyIntent(text: string): boolean { - return APPLY_INTENT_PATTERNS.some((re) => re.test(text)); -} function AssistantWorkspace() { const aui = useAui(); @@ -227,8 +204,6 @@ function AssistantWorkspace() { const { applied, setApplied } = useAppliedTraining(activeSessionId); const { close: closeActivityPanel } = useActivityPanel(); const showPipeline = skill.enabled && applied; - const messages = useAuiState((s) => s.thread.messages); - const lastProcessedUserMsgId = useRef(null); const pipelineContainerRef = useRef(null); const lastPipelineYRef = useRef(null); const [exitButtonVisible, setExitButtonVisible] = useState(false); @@ -354,110 +329,16 @@ function AssistantWorkspace() { closeActivityPanel(); }, [showPipeline, closeActivityPanel]); - // Skill 关掉时撤销 applied,避免重新启用就立刻弹开。 + // Skill 开关联动 applied:开 → 立刻展开面板;关 → 收起。 + // 面板只在用户显式点击 skill 按钮时展开,不再从消息/pipeline 自动推断。 useEffect(() => { - if (!skill.enabled && applied) { + if (skill.enabled && !applied) { + setApplied(true); + } else if (!skill.enabled && applied) { setApplied(false); } }, [skill.enabled, applied, setApplied]); - // 自动应用:skill 启用 + 当前 session 后端的 pipeline 已经有进展(任何 - // 卡 running 或 complete)→ applied=true。这样关 tab 重开 / 刷新 / 切回 - // 已在跑的会话都能自动恢复 monitor 面板,不依赖触发词。 - // 30 秒轮询一次,覆盖「页面打开时 pipeline 还没起、之后才被外部脚本启动」 - // 这类情形——一次性 fetch 容易错过。 - useEffect(() => { - if (!skill.enabled) return; - if (applied) return; - if (!activeSessionId) return; - let cancelled = false; - const pipelineUrl = `/api/claw/training/pipeline?session_id=${encodeURIComponent(activeSessionId)}`; - const runUrl = `/api/claw/runs/latest?session_id=${encodeURIComponent(activeSessionId)}`; - const tick = async () => { - try { - const res = await fetch(pipelineUrl, { cache: "no-store" }); - if (res.ok) { - const payload = await res.json(); - if (cancelled) return; - if (payload) { - const items = Array.isArray(payload.items) ? payload.items : []; - const hasItemProgress = items.some( - (it: { - type?: string; - status?: string; - cards?: Array<{ status?: string }>; - }) => { - if (it.type === "gate") { - return it.status === "running" || it.status === "complete"; - } - return ( - Array.isArray(it.cards) && - it.cards.some( - (c) => - c.status === "running" || c.status === "complete", - ) - ); - }, - ); - // items[] 走 phase-complete-only 过滤;R0 只有一张卡在跑时 - // items 是空的,进展信号在 in_flight 字段里——也算进展。 - const hasInFlight = Boolean(payload.in_flight); - if (hasItemProgress || hasInFlight) { - setApplied(true); - return; - } - } - } - } catch { - /* ignore, fall through to runs/latest fallback */ - } - // Pipeline 还没写出 items/in_flight(agent 处于 prep 阶段,比如在 watcher - // 里 sleep 等远端文件)时,pipeline 端点会返回 state=pending、items=[], - // 但后端 run 其实已经在跑。直接看 run 状态兜底,免得用户起了训练但 - // monitor 面板等到第一个 phase 才弹。 - try { - const res = await fetch(runUrl, { cache: "no-store" }); - if (!res.ok) return; - const payload = await res.json(); - if (cancelled || !payload) return; - if (payload.status === "running" || payload.status === "queued") { - setApplied(true); - } - } catch { - /* ignore */ - } - }; - tick(); - const id = window.setInterval(tick, 30000); - return () => { - cancelled = true; - window.clearInterval(id); - }; - }, [skill.enabled, applied, activeSessionId, setApplied]); - - // 监听用户消息:skill 启用 + 检测到训练触发短语 → applied = true。 - useEffect(() => { - if (!skill.enabled) return; - if (applied) return; - if (!Array.isArray(messages) || messages.length === 0) return; - // 找最新一条 user 消息 - let latestUser: (typeof messages)[number] | null = null; - for (let i = messages.length - 1; i >= 0; i -= 1) { - const m = messages[i]; - if (m.role === "user") { - latestUser = m; - break; - } - } - if (!latestUser) return; - if (lastProcessedUserMsgId.current === latestUser.id) return; - lastProcessedUserMsgId.current = latestUser.id; - const text = collectUserMessageText(latestUser); - if (text && detectApplyIntent(text)) { - setApplied(true); - } - }, [messages, skill.enabled, applied, setApplied]); - // 注意:用同一棵树 + 条件渲染,保证 在 // pipeline 切换前后都保持挂载,否则它们会 remount, // useJupyterWorkspaceStatus 的内部 state 会被清空,UI 上 Jupyter 工作区 @@ -503,21 +384,6 @@ function AssistantWorkspace() { ); } -function collectUserMessageText(message: { content?: unknown }): string { - const content = message.content; - if (typeof content === "string") return content; - if (!Array.isArray(content)) return ""; - const parts: string[] = []; - for (const part of content) { - if (typeof part === "string") { - parts.push(part); - } else if (part && typeof part === "object") { - const text = (part as { text?: unknown }).text; - if (typeof text === "string") parts.push(text); - } - } - return parts.join("\n"); -} function ActivityDrawerTrigger() { const { open, openActivity } = useActivityPanel();