Commit e2a38baf by luoqi

feat(分配): 确认单上的引导节点区 —— 按钮与助手共用同一组 intent

- confirm-sheet-signals.tsx:纯呈现。action 层**全部展开**、info 层折叠成一行,
  🔴 防噪音靠分层 不靠丢弃 —— 藏起一条主管就不知道有东西卡着。
  每条都显示「不处理会怎样」,让「一条不点直接确认」成为**看得见**的安全选项。
- 位置紧贴汇总行, 不放卡片底部:主管视线从汇总往下走,"还有什么要我定的"
  必须在这一跳被看到 —— 埋到底部等于没有(那正是待分配当初被漏掉的原因)。
- intent 路由(chat-blocks,纯函数 + 回归):
  · 待分配三动作 / 改时效 → 翻成 SheetEditOp[],压进和 `edit_assignment_sheet`
    **同一条 edits 队列**。 不给按钮另开执行路径 —— 两条路各做各的必然漂。
  · 换人群 / 换基数 → 必须重跑算法,替主管说那句话交给助手。
     不能用局部改单去凑:凑出来人群没变,他以为条件生效了其实没有(静默错)。
  · 重排一版 / 改时效由卡片就地做(它本来就有实现)。
- 🔴 owner / balance 由按钮直出,没有理解环节 —— 实测栽过:主管说「各自分给
  各自的专属客服」,助手用了铺平,18 个人被散给 17 位别人。

测试:web 13 passed(新增 5 条 intent 路由,含"两张表不能都认领同一个 intent");
service 1230 passed;tsc + build 干净。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 5eb72b84
...@@ -19,6 +19,7 @@ import { assignmentsApi } from '@/components/plans/assignments-api'; ...@@ -19,6 +19,7 @@ import { assignmentsApi } from '@/components/plans/assignments-api';
import { useAssignmentSyncStore } from '@/stores/assignment-sync-store'; import { useAssignmentSyncStore } from '@/stores/assignment-sync-store';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ConfirmSheetSignals } from './confirm-sheet-signals';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { import {
Select, Select,
...@@ -239,6 +240,7 @@ export function AssignmentConfirmSheet({ ...@@ -239,6 +240,7 @@ export function AssignmentConfirmSheet({
edits, edits,
onEditApplied, onEditApplied,
onSheetReplaced, onSheetReplaced,
onIntent,
}: { }: {
requestId: string; requestId: string;
sheet: AssignmentProposal; sheet: AssignmentProposal;
...@@ -260,6 +262,14 @@ export function AssignmentConfirmSheet({ ...@@ -260,6 +262,14 @@ export function AssignmentConfirmSheet({
* ⚠️ 换的是**整张卡**:人群没变、人换了,主管在旧卡上做的删/改派对新名单没有意义。 * ⚠️ 换的是**整张卡**:人群没变、人换了,主管在旧卡上做的删/改派对新名单没有意义。
*/ */
onSheetReplaced?: (next: AssignmentProposal) => void; onSheetReplaced?: (next: AssignmentProposal) => void;
/**
* ⭐ 引导节点上的选项被点了 —— **按钮与助手共用同一组 intent**。
*
* 卡片自己只处理两件它本来就会做的事(重排一版 / 改时效),其余**原样抛给上层**:
* 上层要么把它翻成 `SheetEditOp[]` 压进和助手同一条 edits 队列,要么交给助手重出一版。
* ⛔ 别在这里为每个 intent 各写一套执行 —— 那就等于两条路各做各的,必然漂。
*/
onIntent?: (intent: string, args?: Record<string, unknown>) => void;
}) { }) {
/** /**
* 批次默认时效 —— 初值来自**提案**(沿用主管上一次的值),⛔ 不是写死的 3。 * 批次默认时效 —— 初值来自**提案**(沿用主管上一次的值),⛔ 不是写死的 3。
...@@ -948,6 +958,39 @@ export function AssignmentConfirmSheet({ ...@@ -948,6 +958,39 @@ export function AssignmentConfirmSheet({
}, [edits]); }, [edits]);
/// 找到离卡片最近的可滚动祖先(助手窗的消息区),拖拽自动滚动用 /// 找到离卡片最近的可滚动祖先(助手窗的消息区),拖拽自动滚动用
/**
* 引导节点的选项路由。
* ⚠️ 只截下卡片**本来就有实现**的两个,其余一律上抛 —— 见 `onIntent` 的注释。
*/
const [busyIntent, setBusyIntent] = useState<string | null>(null);
const handleIntent = async (intent: string, args?: Record<string, unknown>) => {
if (intent === 'expiry.set' && typeof args?.days === 'number') {
setExpiresInDays(args.days);
return;
}
if (intent === 'pending.refill') {
setBusyIntent(intent);
try {
// ⚠️ 条件原样带回 —— 少带一个,重排就换了人群
const next = await assignmentsApi.refill({
clinicId: sheet.clinicId,
potentialTreatment: sheet.potentialTreatment,
temperature: sheet.temperature,
anchorMode: sheet.anchorMode,
targetCount: sheet.batchSize,
expiresInDays,
});
onSheetReplaced?.(next);
} catch (e) {
setError(e instanceof Error ? e.message : '重新排版失败');
} finally {
setBusyIntent(null);
}
return;
}
onIntent?.(intent, args);
};
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
let el = rootRef.current?.parentElement ?? null; let el = rootRef.current?.parentElement ?? null;
...@@ -1054,6 +1097,19 @@ export function AssignmentConfirmSheet({ ...@@ -1054,6 +1097,19 @@ export function AssignmentConfirmSheet({
</div> </div>
{/* {/*
①.6 **引导节点** —— 紧贴汇总行,⛔ 不能放卡片底部。
主管的视线从汇总行往下走,"还有什么要我定的"必须在这一跳里被看到;
埋到底部就等于没有(那正是「待分配」当初被漏掉的原因)。
⚠️ 判定在服务端(signals 随提案下发),这里只呈现 + 把 intent 抛给 handleIntent。
*/}
<ConfirmSheetSignals
signals={sheet.signals ?? []}
readOnly={readOnly}
busyIntent={busyIntent}
onIntent={handleIntent}
/>
{/*
①.5 **「重新排一版」做了什么** —— 紧贴汇总行,⛔ 不能放到卡片底部。 ①.5 **「重新排一版」做了什么** —— 紧贴汇总行,⛔ 不能放到卡片底部。
🔴 由来(2026-08-06 实测):点了重排,卡片从「拟分 16 · 待分配 34」变成「拟分 50」, 🔴 由来(2026-08-06 实测):点了重排,卡片从「拟分 16 · 待分配 34」变成「拟分 50」,
而**上面那段话还是旧的**(仍写着"34 人要您定"、"19 人无主")—— 重排走 HTTP 而**上面那段话还是旧的**(仍写着"34 人要您定"、"19 人无主")—— 重排走 HTTP
......
...@@ -198,6 +198,7 @@ function BlockView({ ...@@ -198,6 +198,7 @@ function BlockView({
block, block,
onSheetConfirmed, onSheetConfirmed,
onSheetEdited, onSheetEdited,
onSignalIntent,
onSheetReplaced, onSheetReplaced,
}: { }: {
block: Block; block: Block;
...@@ -206,6 +207,8 @@ function BlockView({ ...@@ -206,6 +207,8 @@ function BlockView({
onSheetEdited?: (summary: string) => void; onSheetEdited?: (summary: string) => void;
/** 「重新排一版」回来的新提案 —— 整块换掉那张卡 */ /** 「重新排一版」回来的新提案 —— 整块换掉那张卡 */
onSheetReplaced?: (requestId: string, next: AssignmentProposal) => void; onSheetReplaced?: (requestId: string, next: AssignmentProposal) => void;
/** ⭐ 引导节点按钮 —— 与助手共用同一组 intent(见 chat-blocks.intentToSheetOps) */
onSignalIntent?: (intent: string, args?: Record<string, unknown>) => void;
}) { }) {
// 工具调用不再单独显示卡片 —— 其"正在做什么"通过下方 loading 指示体现(见 MessageView)。 // 工具调用不再单独显示卡片 —— 其"正在做什么"通过下方 loading 指示体现(见 MessageView)。
if (block.kind === 'tool') return null; if (block.kind === 'tool') return null;
...@@ -221,6 +224,7 @@ function BlockView({ ...@@ -221,6 +224,7 @@ function BlockView({
// 执行结果回一句进对话:既给主管看,也补进模型上下文 —— // 执行结果回一句进对话:既给主管看,也补进模型上下文 ——
// 否则它不知道到底成没成(可能有人没找到),下一句就会替系统撒谎 // 否则它不知道到底成没成(可能有人没找到),下一句就会替系统撒谎
onEditApplied={onSheetEdited} onEditApplied={onSheetEdited}
onIntent={onSignalIntent}
onSheetReplaced={(next) => onSheetReplaced?.(block.requestId, next)} onSheetReplaced={(next) => onSheetReplaced?.(block.requestId, next)}
onConfirmed={(id, summary, modelSummary) => onConfirmed={(id, summary, modelSummary) =>
onSheetConfirmed?.(block.requestId, id, summary, modelSummary) onSheetConfirmed?.(block.requestId, id, summary, modelSummary)
...@@ -477,6 +481,7 @@ function MessageView({ ...@@ -477,6 +481,7 @@ function MessageView({
streaming, streaming,
onSheetConfirmed, onSheetConfirmed,
onSheetEdited, onSheetEdited,
onSignalIntent,
onSheetReplaced, onSheetReplaced,
}: { }: {
message: ChatMessage; message: ChatMessage;
...@@ -486,6 +491,8 @@ function MessageView({ ...@@ -486,6 +491,8 @@ function MessageView({
onSheetEdited?: (summary: string) => void; onSheetEdited?: (summary: string) => void;
/** 「重新排一版」回来的新提案 —— 整块换掉那张卡 */ /** 「重新排一版」回来的新提案 —— 整块换掉那张卡 */
onSheetReplaced?: (requestId: string, next: AssignmentProposal) => void; onSheetReplaced?: (requestId: string, next: AssignmentProposal) => void;
/** ⭐ 引导节点按钮 —— 与助手共用同一组 intent(见 chat-blocks.intentToSheetOps) */
onSignalIntent?: (intent: string, args?: Record<string, unknown>) => void;
}) { }) {
if (message.role === 'user') { if (message.role === 'user') {
return ( return (
...@@ -542,6 +549,7 @@ function MessageView({ ...@@ -542,6 +549,7 @@ function MessageView({
block={b} block={b}
onSheetConfirmed={onSheetConfirmed} onSheetConfirmed={onSheetConfirmed}
onSheetEdited={onSheetEdited} onSheetEdited={onSheetEdited}
onSignalIntent={onSignalIntent}
onSheetReplaced={onSheetReplaced} onSheetReplaced={onSheetReplaced}
/> />
))} ))}
...@@ -633,6 +641,7 @@ export function AssistantChat({ ...@@ -633,6 +641,7 @@ export function AssistantChat({
appendAssistantNote, appendAssistantNote,
settleSheet, settleSheet,
replaceSheet, replaceSheet,
applySignalIntent,
} = useAssistantChat(); } = useAssistantChat();
/** /**
...@@ -917,6 +926,7 @@ export function AssistantChat({ ...@@ -917,6 +926,7 @@ export function AssistantChat({
onSheetConfirmed={onSheetConfirmed} onSheetConfirmed={onSheetConfirmed}
onSheetEdited={onSheetEdited} onSheetEdited={onSheetEdited}
onSheetReplaced={onSheetReplaced} onSheetReplaced={onSheetReplaced}
onSignalIntent={applySignalIntent}
/> />
)) ))
)} )}
......
...@@ -3,6 +3,8 @@ import type { AssignmentProposal } from '@pac/types'; ...@@ -3,6 +3,8 @@ import type { AssignmentProposal } from '@pac/types';
import { import {
applyIncomingSheet, applyIncomingSheet,
findActiveSheet, findActiveSheet,
intentToPrompt,
intentToSheetOps,
type Block, type Block,
type ChatMessage, type ChatMessage,
type DraftState, type DraftState,
...@@ -132,3 +134,57 @@ describe('findActiveSheet', () => { ...@@ -132,3 +134,57 @@ describe('findActiveSheet', () => {
expect(findActiveSheet([msg('m1')])).toBeNull(); expect(findActiveSheet([msg('m1')])).toBeNull();
}); });
}); });
/**
* intent 路由 —— 🔴 **按钮和助手必须走同一条路**。
*
* 这里锁的是"哪些 intent 走局部改单、哪些走重跑",以及 owner/balance 不能弄反
* (实测栽过:主管说「各自分给各自的专属客服」,助手用了铺平,18 人被散给 17 位别人)。
*/
describe('引导节点 intent 路由', () => {
it('待分配三个动作 → 局部改单,且 owner / balance 不弄反', () => {
expect(intentToSheetOps('pending.to_owner')).toEqual([
{ select: { group: 'pending' }, action: 'assign', to: { mode: 'owner' } },
]);
expect(intentToSheetOps('pending.spread')).toEqual([
{ select: { group: 'pending' }, action: 'assign', to: { mode: 'balance' } },
]);
expect(intentToSheetOps('pending.remove')).toEqual([
{ select: { group: 'pending' }, action: 'remove' },
]);
});
it('改时效 → 局部改单;缺 days → null(⛔ 不许瞎猜一个天数)', () => {
expect(intentToSheetOps('expiry.set', { days: 5 })).toEqual([
{ select: { group: 'batch' }, action: 'set_expiry', days: 5 },
]);
expect(intentToSheetOps('expiry.set')).toBeNull();
});
it('🔴 换人群 / 换基数 → ⛔ 不能局部改单,必须走重跑', () => {
for (const i of ['cohort.narrow', 'cohort.widen', 'anchor.switch', 'batch_size.set', 'daily_rate.set']) {
expect(intentToSheetOps(i)).toBeNull();
}
expect(intentToPrompt('cohort.widen')).toContain('重新排一版');
expect(intentToPrompt('anchor.switch')).toContain('重新排一版');
expect(intentToPrompt('cohort.narrow', { personaTags: 'rfm:vip' })).toContain('rfm:vip');
});
it('⛔ 两张表不能都认领同一个 intent(否则按钮行为取决于判断顺序)', () => {
const all = [
'pending.to_owner', 'pending.spread', 'pending.remove', 'pending.refill',
'expiry.set', 'daily_rate.set', 'batch_size.set',
'cohort.widen', 'cohort.narrow', 'anchor.switch',
];
for (const i of all) {
const ops = intentToSheetOps(i, { days: 5, personaTags: 'rfm:vip' });
const text = intentToPrompt(i, { days: 5, personaTags: 'rfm:vip' });
expect(ops === null || text === null).toBe(true);
}
});
it('未知 intent → 两边都 null(卡片静默不动,⛔ 不许乱执行)', () => {
expect(intentToSheetOps('nope.nope')).toBeNull();
expect(intentToPrompt('nope.nope')).toBeNull();
});
});
...@@ -189,3 +189,68 @@ export function findActiveSheet( ...@@ -189,3 +189,68 @@ export function findActiveSheet(
} }
return null; return null;
} }
// ─────────────────────────────────────────────────────────
// 引导节点的 intent 路由
// ─────────────────────────────────────────────────────────
/**
* intent → 卡片上的语义编辑指令。
*
* ⭐ **按钮和助手走的是同一条队列**:助手的 `edit_assignment_sheet` 推的是
* `SheetEditOp[]`,这里把按钮点击也翻成同一种 op 压进去。
* ⛔ 别为按钮另写一套执行路径 —— 两条路各做各的,行为必然漂,而漂了不报错。
*
* 返回 `null` = 这个 intent 不是局部改单能完成的(要重跑算法),交给 `intentToPrompt`。
*/
export function intentToSheetOps(
intent: string,
args?: Record<string, unknown>,
): SheetEditOp[] | null {
switch (intent) {
case 'pending.to_owner':
// ⚠️ owner 与 balance **结果正好相反**,选错主管一眼看得出来(实测栽过:
// 18 个人被散给了 17 位别人)。按钮这条路没有理解环节,所以不会选错。
return [{ select: { group: 'pending' }, action: 'assign', to: { mode: 'owner' } }];
case 'pending.spread':
return [{ select: { group: 'pending' }, action: 'assign', to: { mode: 'balance' } }];
case 'pending.remove':
return [{ select: { group: 'pending' }, action: 'remove' }];
case 'expiry.set':
return typeof args?.days === 'number'
? [{ select: { group: 'batch' }, action: 'set_expiry', days: args.days }]
: null;
default:
return null;
}
}
/**
* intent → 替主管说的那句话(交给助手重跑算法)。
*
* ⚠️ 这几个都要**换人群或换基数**,必须重出一版,⛔ 不能用局部改单去凑
* (凑出来的单子人群没变,主管以为条件生效了、其实没有 —— 静默错)。
* ⚠️ 措辞用**主管会说的话**,⛔ 不带 intent id、⛔ 不带字段名 ——
* 这句会原样出现在对话里,他要能认出这是自己刚点的那一下。
*/
export function intentToPrompt(
intent: string,
args?: Record<string, unknown>,
): string | null {
switch (intent) {
case 'cohort.narrow':
return typeof args?.personaTags === 'string'
? `这批只圈符合「${args.personaTags}」的人,重新排一版`
: null;
case 'cohort.widen':
return '往后放一档再看看,或者不限时间档,重新排一版';
case 'anchor.switch':
return '换成按诊断距今的口径,重新排一版';
case 'batch_size.set':
return '这批人数减少一些,重新排一版';
case 'daily_rate.set':
return '每人每天按更少的通数算,重新排一版';
default:
return null;
}
}
'use client';
import { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { Signal } from '@pac/types';
import { Button } from '@/components/ui/button';
/**
* 引导节点区 —— 确认单上「还有什么要您定的」。
*
* 规格见 docs/design/assignment-agent-flow.md §五。这一层是**纯呈现**:
* ⛔ 不判定该不该亮(那是服务端 `computeSignals` 的事),⛔ 不执行动作(只把 intent 抛上去)。
*
* ── 三条来自教条的硬要求 ────────────────────────────────────────
* ① **不截断**:`action` 层全部展开。防噪音靠把 `info` 折叠,⛔ 不靠丢弃 ——
* 藏起一条主管就不知道有东西卡着(G9)。
* ② **默认永远是 no-op 且要写出来**:每条都显示"不处理会怎样",
* 这样「一条不点、直接确认」是**看得见**的安全选项(G4/G8)。
* ③ **选项是可点击的动作,不是让他照着打字**(G10)。
* 打字 → 模型理解 → 翻译成动作,每一环都可能错;点击是确定性的。
*/
export function ConfirmSheetSignals({
signals,
readOnly,
busyIntent,
onIntent,
}: {
signals: Signal[];
readOnly: boolean;
/** 正在执行的 intent(按钮转圈用);null = 空闲 */
busyIntent?: string | null;
onIntent: (intent: string, args?: Record<string, unknown>) => void;
}) {
const [infoOpen, setInfoOpen] = useState(false);
if (signals.length === 0) return null;
const action = signals.filter((s) => s.tier === 'action');
const info = signals.filter((s) => s.tier === 'info');
return (
<div className="border-b bg-amber-50/40">
{action.map((s) => (
<Row key={s.key} signal={s} readOnly={readOnly} busyIntent={busyIntent} onIntent={onIntent} />
))}
{/*
⚠️ 纯信息类默认折叠 —— 它们「不管也没事」,展开会把真正要处置的那几条挤下去。
⛔ 但不能不给:主管看到「405 人」时,"这数哪来的"和这个数本身一样重要(T14)。
*/}
{info.length > 0 && (
<div className="px-3 py-1.5">
<button
type="button"
onClick={() => setInfoOpen((v) => !v)}
className="flex items-center gap-1 text-[11px] text-slate-500 hover:text-slate-700"
>
{infoOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
另有 {info.length} 条说明
</button>
{infoOpen &&
info.map((s) => (
<Row
key={s.key}
signal={s}
readOnly={readOnly}
busyIntent={busyIntent}
onIntent={onIntent}
muted
/>
))}
</div>
)}
</div>
);
}
function Row({
signal: s,
readOnly,
busyIntent,
onIntent,
muted,
}: {
signal: Signal;
readOnly: boolean;
busyIntent?: string | null;
onIntent: (intent: string, args?: Record<string, unknown>) => void;
muted?: boolean;
}) {
return (
<div className={`px-3 py-2 ${muted ? 'pt-1.5' : 'border-b border-amber-100/70 last:border-b-0'}`}>
{/* ⚠️ 标题是**唯一**加粗的地方:一条消息里加粗超过两处就等于没有重点 */}
<p className={muted ? 'text-[11.5px] text-slate-600' : 'text-[12px] font-medium text-slate-800'}>
{s.title}
</p>
<p className="mt-0.5 text-[11px] leading-relaxed text-slate-500">{s.why}</p>
{/*
⭐ 默认路径**必须显示**,而且要看起来是个正常选项而不是失败兜底 ——
「一条不点直接确认」在任何情况下都安全,这句话是它的证据。
*/}
<p className="mt-0.5 text-[10.5px] text-slate-400">{s.defaultLabel}</p>
{!readOnly && s.options.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1.5">
{s.options.map((o) => {
const busy = busyIntent === o.intent;
return (
<Button
key={`${o.intent}:${o.label}`}
size="sm"
variant="outline"
disabled={busyIntent != null}
onClick={() => onIntent(o.intent, o.args)}
className="h-6 border-amber-300 bg-white px-2 text-[11px] text-amber-900 hover:bg-amber-50"
>
{busy ? '处理中…' : o.label}
</Button>
);
})}
</div>
)}
</div>
);
}
...@@ -12,6 +12,8 @@ import { ...@@ -12,6 +12,8 @@ import {
applyIncomingSheet, applyIncomingSheet,
findActiveSheet, findActiveSheet,
findToolIdx, findToolIdx,
intentToPrompt,
intentToSheetOps,
upsertArtifact, upsertArtifact,
type Block, type Block,
type ChatMessage, type ChatMessage,
...@@ -427,6 +429,40 @@ export function useAssistantChat() { ...@@ -427,6 +429,40 @@ export function useAssistantChat() {
); );
}, []); }, []);
/**
* ⭐ 引导节点上的按钮被点了。
*
* 🔴 **和助手走同一条路**:能局部改单的翻成 `SheetEditOp[]` 压进和
* `edit_assignment_sheet` 同一条 edits 队列;要重跑算法的就替主管说那句话。
* ⛔ 别给按钮另开一条执行路径 —— 两条路各做各的必然漂,而漂了不报错。
* ⚠️ 走「说那句话」这条时,那句话会**原样出现在对话里** —— 主管要能认出
* 这是自己刚点的那一下,⛔ 不许做成隐藏指令(界面替他说话他就无法纠正)。
*/
const applySignalIntent = useCallback(
(intent: string, args?: Record<string, unknown>) => {
const ops = intentToSheetOps(intent, args);
if (ops) {
setMessages((prev) => {
const at = findActiveSheet(prev);
if (!at) return prev;
const { messageIndex: mi, blockIndex: bi } = at;
const b = prev[mi]!.blocks[bi] as Extract<Block, { kind: 'assignment_sheet' }>;
const next: Block = {
...b,
edits: [...(b.edits ?? []), { seq: (b.edits?.length ?? 0) + 1, ops }],
};
return prev.map((m, k) =>
k === mi ? { ...m, blocks: m.blocks.map((x, j) => (j === bi ? next : x)) } : m,
);
});
return;
}
const text = intentToPrompt(intent, args);
if (text) void send(text);
},
[send],
);
/** 确认单落库后把卡片切到终态(按钮禁用,显示批次号) */ /** 确认单落库后把卡片切到终态(按钮禁用,显示批次号) */
const settleSheet = useCallback((requestId: string, assignmentId: string) => { const settleSheet = useCallback((requestId: string, assignmentId: string) => {
setMessages((prev) => setMessages((prev) =>
...@@ -441,5 +477,16 @@ export function useAssistantChat() { ...@@ -441,5 +477,16 @@ export function useAssistantChat() {
); );
}, []); }, []);
return { messages, status, model, setModel, send, stop, appendAssistantNote, settleSheet, replaceSheet }; return {
messages,
status,
model,
setModel,
send,
stop,
appendAssistantNote,
settleSheet,
replaceSheet,
applySignalIntent,
};
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment