@fcg-labs/cx-agent-hook 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +116 -0
- package/README.md +55 -0
- package/agent.d.ts +197 -0
- package/agent.js +258 -0
- package/client.d.ts +4 -1
- package/client.js +124 -2
- package/compat.js +67 -0
- package/element.js +25 -43
- package/index.js +10 -328
- package/locales.js +56 -0
- package/package.json +10 -1
- package/react.js +17 -36
- package/session.js +455 -0
- package/styles.css +91 -27
- package/view.d.ts +36 -0
- package/view.js +142 -0
- package/vue.js +18 -31
package/view.js
CHANGED
|
@@ -58,3 +58,145 @@ export const CLS = {
|
|
|
58
58
|
evidence: "fcx-ai-evidence",
|
|
59
59
|
declined: "fcx-ai-declined",
|
|
60
60
|
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 패널 전체의 프레임워크 중립 트리 스펙 (0.3.0).
|
|
64
|
+
*
|
|
65
|
+
* aiSuggestView 가 "무엇을 보여줄지" 였다면 이것은 "어떤 구조로 그릴지"
|
|
66
|
+
* 까지다 — react/vue/element 는 이 트리의 15줄급 인터프리터로 줄어든다.
|
|
67
|
+
* 세 어댑터가 구조를 각자 복제하던 시절에는 하나를 고치면 나머지 둘이
|
|
68
|
+
* 그대로 남았다 (뷰 3중 복제).
|
|
69
|
+
*
|
|
70
|
+
* 노드: { tag, cls, text?, children?, action?, type?, disabled? }
|
|
71
|
+
* action 은 추상 동작명 — 어댑터가 자기 이벤트 체계로 배선한다:
|
|
72
|
+
* "request" 제안 요청 / "adopt" 에디터에 넣기
|
|
73
|
+
* 루트의 클릭 전파 차단(행 선택 토글 방지)은 어댑터 몫이다 — 트리는
|
|
74
|
+
* 구조만 말한다.
|
|
75
|
+
*/
|
|
76
|
+
export function aiSuggestTree(input) {
|
|
77
|
+
const v = aiSuggestView(input);
|
|
78
|
+
const head = {
|
|
79
|
+
tag: "div", cls: CLS.head,
|
|
80
|
+
children: [
|
|
81
|
+
{ tag: "button", cls: CLS.button, type: "button",
|
|
82
|
+
disabled: v.buttonDisabled, text: v.buttonLabel, action: "request" },
|
|
83
|
+
...(v.showAdopt
|
|
84
|
+
? [{ tag: "button", cls: CLS.adopt, type: "button",
|
|
85
|
+
disabled: false, text: v.adoptLabel, action: "adopt" }]
|
|
86
|
+
: []),
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
const children = [head];
|
|
90
|
+
if (v.body.kind === "answer") {
|
|
91
|
+
children.push({
|
|
92
|
+
tag: "div", cls: CLS.body,
|
|
93
|
+
children: [
|
|
94
|
+
{ tag: "div", cls: CLS.answer, text: v.body.text },
|
|
95
|
+
...(v.body.evidence.length > 0
|
|
96
|
+
? [{ tag: "div", cls: CLS.evidence,
|
|
97
|
+
text: `${v.body.evidenceLabel}: ${v.body.evidence.join(" · ")}` }]
|
|
98
|
+
: []),
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
} else if (v.body.kind === "declined") {
|
|
102
|
+
children.push({ tag: "div", cls: CLS.declined, text: v.body.text });
|
|
103
|
+
}
|
|
104
|
+
return { tag: "div", cls: CLS.root, children };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// ── 처리 액션 (actions/1) — 선택지·확인·3상태의 표시 판단 ─────────────────
|
|
109
|
+
//
|
|
110
|
+
// 실행 시맨틱은 여기 없다. offers 는 답변 payload 의 suggested_actions 그대로,
|
|
111
|
+
// pending 은 세션 ActionLedger 그대로 — 이 함수는 "무엇을 어떤 상태로 보일지"
|
|
112
|
+
// 만 결정한다. 텍스트 노드만 (innerHTML 금지 — 고객사 기입 문구가 실린다).
|
|
113
|
+
|
|
114
|
+
export const ACTION_CLS = {
|
|
115
|
+
root: "fcx-act",
|
|
116
|
+
item: "fcx-act-item",
|
|
117
|
+
label: "fcx-act-label",
|
|
118
|
+
params: "fcx-act-params",
|
|
119
|
+
risk: "fcx-act-risk",
|
|
120
|
+
button: "fcx-act-button",
|
|
121
|
+
confirm: "fcx-act-confirm",
|
|
122
|
+
confirmText: "fcx-act-confirm-text",
|
|
123
|
+
ack: "fcx-act-ack",
|
|
124
|
+
status: "fcx-act-status",
|
|
125
|
+
reconcile: "fcx-act-reconcile",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const RISK_KEY = { low: "ui_action_risk_low", medium: "ui_action_risk_medium", high: "ui_action_risk_high" };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {Array} offers session.offers
|
|
132
|
+
* @param {Array} pending session.pendingActions() [{requestId, offerId, state, reasonCode, message, label}]
|
|
133
|
+
* @param {boolean} enabled agent.actionsEnabled
|
|
134
|
+
* @param {string|null} confirming 확인 패널이 열린 offer_id
|
|
135
|
+
* @param {Record<string,string>} messages
|
|
136
|
+
*/
|
|
137
|
+
export function actionOffersView({ offers = [], pending = [], enabled = true, confirming = null, messages = {} }) {
|
|
138
|
+
const byOffer = new Map();
|
|
139
|
+
for (const p of pending) byOffer.set(p.offerId, p);
|
|
140
|
+
const items = offers.map((o) => {
|
|
141
|
+
const p = byOffer.get(o.offer_id) || null;
|
|
142
|
+
const state = p ? p.state : "";
|
|
143
|
+
const terminal = state === "succeeded" || state === "failed";
|
|
144
|
+
let statusText = "";
|
|
145
|
+
if (state === "pending") statusText = messages.ui_action_running;
|
|
146
|
+
else if (state === "succeeded") statusText = messages.ui_action_succeeded;
|
|
147
|
+
else if (state === "failed") statusText = `${messages.ui_action_failed}${p && p.message ? " — " + p.message : ""}`;
|
|
148
|
+
else if (state === "unknown") statusText = messages.ui_action_unknown;
|
|
149
|
+
return {
|
|
150
|
+
offerId: o.offer_id, label: o.label || o.action_key, params: o.params_display || "",
|
|
151
|
+
riskLabel: messages[RISK_KEY[o.risk_level] || RISK_KEY.low] || "",
|
|
152
|
+
risk: o.risk_level || "low",
|
|
153
|
+
// 실행 버튼: 표면 열림 ∧ 미실행(또는 실패-재시도 가능) — unknown 은 재실행 없음
|
|
154
|
+
canExecute: enabled && (!p || (state === "failed")),
|
|
155
|
+
buttonLabel: messages.ui_action_execute,
|
|
156
|
+
confirming: confirming === o.offer_id,
|
|
157
|
+
confirmText: o.confirm || messages.ui_action_confirm_default,
|
|
158
|
+
needsAck: o.risk_level === "high",
|
|
159
|
+
ackLabel: messages.ui_action_ack,
|
|
160
|
+
okLabel: messages.ui_action_confirm_ok,
|
|
161
|
+
cancelLabel: messages.ui_action_confirm_cancel,
|
|
162
|
+
state, statusText, terminal,
|
|
163
|
+
// unknown 만 "다시 확인" (reconcile=lookup) — 재실행 버튼은 없다
|
|
164
|
+
canReconcile: state === "unknown",
|
|
165
|
+
reconcileLabel: messages.ui_action_reconcile,
|
|
166
|
+
requestId: p ? p.requestId : "",
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
return { visible: items.length > 0, items, disabledHint: enabled ? "" : messages.ui_action_disabled };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 프레임워크 중립 트리 — action: "confirm"(패널 열기) / "execute" / "cancel" / "reconcile" / "ack" */
|
|
173
|
+
export function actionOffersTree(input) {
|
|
174
|
+
const v = actionOffersView(input);
|
|
175
|
+
if (!v.visible) return null;
|
|
176
|
+
const items = v.items.map((it) => {
|
|
177
|
+
const children = [
|
|
178
|
+
{ tag: "div", cls: ACTION_CLS.label, text: it.label },
|
|
179
|
+
...(it.params ? [{ tag: "div", cls: ACTION_CLS.params, text: it.params }] : []),
|
|
180
|
+
{ tag: "span", cls: ACTION_CLS.risk, text: it.riskLabel, risk: it.risk },
|
|
181
|
+
];
|
|
182
|
+
if (it.confirming) {
|
|
183
|
+
children.push({
|
|
184
|
+
tag: "div", cls: ACTION_CLS.confirm, children: [
|
|
185
|
+
{ tag: "div", cls: ACTION_CLS.confirmText, text: it.confirmText },
|
|
186
|
+
...(it.needsAck ? [{ tag: "label", cls: ACTION_CLS.ack, text: it.ackLabel, action: "ack", offerId: it.offerId }] : []),
|
|
187
|
+
{ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.okLabel, action: "execute", offerId: it.offerId },
|
|
188
|
+
{ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.cancelLabel, action: "cancel", offerId: it.offerId },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
} else if (it.canExecute) {
|
|
192
|
+
children.push({ tag: "button", cls: ACTION_CLS.button, type: "button", text: it.buttonLabel, action: "confirm", offerId: it.offerId });
|
|
193
|
+
}
|
|
194
|
+
if (it.statusText) children.push({ tag: "div", cls: ACTION_CLS.status, text: it.statusText, state: it.state });
|
|
195
|
+
if (it.canReconcile) {
|
|
196
|
+
children.push({ tag: "button", cls: ACTION_CLS.reconcile, type: "button", text: it.reconcileLabel, action: "reconcile", requestId: it.requestId });
|
|
197
|
+
}
|
|
198
|
+
return { tag: "div", cls: ACTION_CLS.item, children };
|
|
199
|
+
});
|
|
200
|
+
if (v.disabledHint) items.push({ tag: "div", cls: ACTION_CLS.status, text: v.disabledHint });
|
|
201
|
+
return { tag: "div", cls: ACTION_CLS.root, children: items };
|
|
202
|
+
}
|
package/vue.js
CHANGED
|
@@ -21,10 +21,24 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { computed, defineComponent, h, ref, watch } from "vue";
|
|
23
23
|
|
|
24
|
-
import { aiSuggestView, CLS } from "./view.js";
|
|
24
|
+
import { aiSuggestTree, aiSuggestView, CLS } from "./view.js";
|
|
25
25
|
|
|
26
26
|
export { aiSuggestView };
|
|
27
27
|
|
|
28
|
+
/** 트리 노드 → VNode — 구조는 view.aiSuggestTree 가 소유한다. */
|
|
29
|
+
function renderNode(node, actions) {
|
|
30
|
+
const props = { class: node.cls };
|
|
31
|
+
if (node.tag === "button") {
|
|
32
|
+
props.type = node.type || "button";
|
|
33
|
+
props.disabled = Boolean(node.disabled);
|
|
34
|
+
}
|
|
35
|
+
if (node.action && actions[node.action]) props.onClick = actions[node.action];
|
|
36
|
+
return h(
|
|
37
|
+
node.tag, props,
|
|
38
|
+
node.children ? node.children.map((c) => renderNode(c, actions)) : node.text,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
export const AiSuggestPanel = defineComponent({
|
|
29
43
|
name: "AiSuggestPanel",
|
|
30
44
|
props: {
|
|
@@ -68,8 +82,8 @@ export const AiSuggestPanel = defineComponent({
|
|
|
68
82
|
emit("adopt", r.answer);
|
|
69
83
|
};
|
|
70
84
|
|
|
71
|
-
const
|
|
72
|
-
|
|
85
|
+
const tree = computed(() =>
|
|
86
|
+
aiSuggestTree({
|
|
73
87
|
state: state.value,
|
|
74
88
|
result: result.value,
|
|
75
89
|
declineText: props.hook.declineText,
|
|
@@ -79,7 +93,6 @@ export const AiSuggestPanel = defineComponent({
|
|
|
79
93
|
|
|
80
94
|
return () => {
|
|
81
95
|
if (!props.hook || !props.hook.enabled) return null;
|
|
82
|
-
const v = view.value;
|
|
83
96
|
return h(
|
|
84
97
|
"div",
|
|
85
98
|
{
|
|
@@ -88,33 +101,7 @@ export const AiSuggestPanel = defineComponent({
|
|
|
88
101
|
// 누르려다 행 선택이 토글되면 제안이 초기화된다.
|
|
89
102
|
onClick: (event) => event.stopPropagation(),
|
|
90
103
|
},
|
|
91
|
-
|
|
92
|
-
h("div", { class: CLS.head }, [
|
|
93
|
-
h(
|
|
94
|
-
"button",
|
|
95
|
-
{ type: "button", class: CLS.button, disabled: v.buttonDisabled, onClick: request },
|
|
96
|
-
v.buttonLabel,
|
|
97
|
-
),
|
|
98
|
-
v.showAdopt
|
|
99
|
-
? h("button", { type: "button", class: CLS.adopt, onClick: adopt }, v.adoptLabel)
|
|
100
|
-
: null,
|
|
101
|
-
]),
|
|
102
|
-
v.body.kind === "answer"
|
|
103
|
-
? h("div", { class: CLS.body }, [
|
|
104
|
-
h("div", { class: CLS.answer }, v.body.text),
|
|
105
|
-
v.body.evidence.length > 0
|
|
106
|
-
? h(
|
|
107
|
-
"div",
|
|
108
|
-
{ class: CLS.evidence },
|
|
109
|
-
`${v.body.evidenceLabel}: ${v.body.evidence.join(" · ")}`,
|
|
110
|
-
)
|
|
111
|
-
: null,
|
|
112
|
-
])
|
|
113
|
-
: null,
|
|
114
|
-
v.body.kind === "declined"
|
|
115
|
-
? h("div", { class: CLS.declined }, v.body.text)
|
|
116
|
-
: null,
|
|
117
|
-
],
|
|
104
|
+
tree.value.children.map((node) => renderNode(node, { request, adopt })),
|
|
118
105
|
);
|
|
119
106
|
};
|
|
120
107
|
},
|