@fcg-labs/cx-agent-hook 0.7.1 → 0.8.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 +14 -0
- package/agent.d.ts +9 -0
- package/client.d.ts +3 -0
- package/client.js +13 -7
- package/index.d.ts +6 -0
- package/locales.js +12 -0
- package/package.json +1 -1
- package/react.d.ts +9 -0
- package/react.js +35 -1
- package/session.js +46 -2
- package/view.d.ts +8 -0
- package/view.js +46 -0
package/MIGRATION.md
CHANGED
|
@@ -214,3 +214,17 @@ headless 2단계: ① 스타일 0 — styles.css 를 import 하지 않는다.
|
|
|
214
214
|
이후 허브 발급분은 문자열이다. 불투명 취급하면 코드 변경은 없다.
|
|
215
215
|
- element 어댑터의 내부 DOM 구조는 동일하나, 어댑터 3종이 `aiSuggestTree`
|
|
216
216
|
인터프리터로 재구현됐다 — 클래스·이벤트·마크업 계약은 시험으로 고정.
|
|
217
|
+
|
|
218
|
+
## 0.7.x → 0.8.0 — 다안 (candidates)
|
|
219
|
+
|
|
220
|
+
전부 **가산적**이라 0.7.x 소비처는 코드 변경 없이 그대로 동작한다.
|
|
221
|
+
|
|
222
|
+
- 응답에 `candidates[]` 가 실린다 — `[0]` 은 항상 본안(`variant: "primary"`,
|
|
223
|
+
`answer` 는 최상위 `answer` 와 동일). 서버 env(`AI_CS_CANDIDATES`)가 꺼져
|
|
224
|
+
있으면 빈 배열이다.
|
|
225
|
+
- 세션: `candidates` / `chosenVariant` getter 와 `choose(index)` — 후보를 초안
|
|
226
|
+
정본으로 바꾼다(데코레이트는 compose 시점에 끝나 있다). sink 에
|
|
227
|
+
`setCandidates(list, chosen)` 를 **선택적으로** 구현하면 도착·선택을 받는다.
|
|
228
|
+
- 발송: 본안이 아닌 후보를 골라 보냈으면 `sent` 피드백의 `note` 에
|
|
229
|
+
`variant:<id>` 가 남는다 — 와이어 계약(허브 Go) 무변경.
|
|
230
|
+
- React: `CandidatePicker` 신규. `AiSuggestPanel` 은 무변경.
|
package/agent.d.ts
CHANGED
|
@@ -81,6 +81,9 @@ export interface ComposeHandle {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export interface SessionUi {
|
|
84
|
+
/** 다안 도착·선택 시 (0.8.0, 선택 구현) — 없으면 조용히 생략된다. */
|
|
85
|
+
setCandidates?(candidates: Array<{ variant: string; label: string; answer: string }>,
|
|
86
|
+
chosen: string): void;
|
|
84
87
|
getDraft?: () => string;
|
|
85
88
|
setDraft: (text: string) => void;
|
|
86
89
|
setStatus?: (text: string) => void;
|
|
@@ -92,6 +95,12 @@ export declare class InquirySession {
|
|
|
92
95
|
readonly state: "idle" | "composing" | "drafted" | "declined";
|
|
93
96
|
readonly draft: string;
|
|
94
97
|
readonly adoptedAnswerId: number | string | null;
|
|
98
|
+
/** 다안 (0.8.0) — 데코레이트 완료본, [0]=본안. */
|
|
99
|
+
readonly candidates: Array<{ variant: string; label: string; answer: string;
|
|
100
|
+
evidence_keys: string[]; contract_warnings: string[] }>;
|
|
101
|
+
readonly chosenVariant: string;
|
|
102
|
+
/** 후보를 초안 정본으로 (0.8.0). 잘못된 index 는 무동작. */
|
|
103
|
+
choose(index: number): void;
|
|
95
104
|
|
|
96
105
|
/** UI sink 연결 — 자동 주입 없음, 복원은 restore() 명시 호출 */
|
|
97
106
|
attachUi(ui: SessionUi): this;
|
package/client.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export interface AnswerResult {
|
|
|
38
38
|
/** 신원 수술(0048) 이후 허브 발급분은 ULID 문자열 — 불투명 취급 */
|
|
39
39
|
answerId: number | string | null;
|
|
40
40
|
evidence: Array<{ unit_key: string; title: string; score: number }>;
|
|
41
|
+
/** 다안 (0.8.0) — [0]=본안. env 끔·거절이면 []. */
|
|
42
|
+
candidates: Array<{ variant: string; label: string; answer: string;
|
|
43
|
+
evidence_keys: string[]; contract_warnings: string[] }>;
|
|
41
44
|
declinedReason: string;
|
|
42
45
|
raw: Record<string, unknown>;
|
|
43
46
|
}
|
package/client.js
CHANGED
|
@@ -206,7 +206,7 @@ export class CxAgentClient {
|
|
|
206
206
|
);
|
|
207
207
|
return {
|
|
208
208
|
ok: false, answered: false, answer: "", answerId: null,
|
|
209
|
-
evidence: [], declinedReason: "unsupported_api", raw: {},
|
|
209
|
+
evidence: [], candidates: [], declinedReason: "unsupported_api", raw: {},
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
try {
|
|
@@ -227,7 +227,7 @@ export class CxAgentClient {
|
|
|
227
227
|
// 공장 JSONResponse({"error": "serving_disabled"}, 503) 등
|
|
228
228
|
return {
|
|
229
229
|
ok: false, answered: false, answer: "", answerId: null,
|
|
230
|
-
evidence: [],
|
|
230
|
+
evidence: [], candidates: [],
|
|
231
231
|
declinedReason: (data && data.error) || `http_${status}`,
|
|
232
232
|
raw: data,
|
|
233
233
|
};
|
|
@@ -238,6 +238,7 @@ export class CxAgentClient {
|
|
|
238
238
|
answer: data.answer || "",
|
|
239
239
|
answerId: data.answer_id ?? null,
|
|
240
240
|
evidence: data.evidence || [],
|
|
241
|
+
candidates: Array.isArray(data.candidates) ? data.candidates : [],
|
|
241
242
|
declinedReason: data.declined_reason || "",
|
|
242
243
|
raw: data,
|
|
243
244
|
};
|
|
@@ -245,7 +246,7 @@ export class CxAgentClient {
|
|
|
245
246
|
this.onError(err, { op: "getAnswer" });
|
|
246
247
|
return {
|
|
247
248
|
ok: false, answered: false, answer: "", answerId: null,
|
|
248
|
-
evidence: [], declinedReason: "network_error", raw: {},
|
|
249
|
+
evidence: [], candidates: [], declinedReason: "network_error", raw: {},
|
|
249
250
|
};
|
|
250
251
|
}
|
|
251
252
|
}
|
|
@@ -276,7 +277,7 @@ export class CxAgentClient {
|
|
|
276
277
|
} = {}) {
|
|
277
278
|
const fail = (reason, raw = {}) => ({
|
|
278
279
|
ok: false, answered: false, answer: "", answerId: null,
|
|
279
|
-
evidence: [], declinedReason: reason, raw,
|
|
280
|
+
evidence: [], candidates: [], declinedReason: reason, raw,
|
|
280
281
|
});
|
|
281
282
|
const target = this._target("answer");
|
|
282
283
|
const streamPath = PATHS[target.api] && PATHS[target.api].answerStream;
|
|
@@ -361,6 +362,7 @@ export class CxAgentClient {
|
|
|
361
362
|
answer: final.answer || "",
|
|
362
363
|
answerId: final.answer_id ?? null,
|
|
363
364
|
evidence: final.evidence || [],
|
|
365
|
+
candidates: Array.isArray(final.candidates) ? final.candidates : [],
|
|
364
366
|
declinedReason: final.declined_reason || "",
|
|
365
367
|
raw: final,
|
|
366
368
|
};
|
|
@@ -445,9 +447,13 @@ export class CxAgentClient {
|
|
|
445
447
|
return { ok: false, feedbackId: null, error: code };
|
|
446
448
|
}
|
|
447
449
|
|
|
448
|
-
/** 발송 후킹 — CS팀이 실제로 내보낸 최종 문구 (gold 학습 쌍의 원천)
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
/** 발송 후킹 — CS팀이 실제로 내보낸 최종 문구 (gold 학습 쌍의 원천).
|
|
451
|
+
* note: 다안(0.8.0)에서 선택 변형 표식 `variant:<id>` — 와이어 계약 무변경. */
|
|
452
|
+
sent(answerId, finalText, agent, note = "") {
|
|
453
|
+
return this.sendFeedback({
|
|
454
|
+
answerId, action: "sent", finalText, agent,
|
|
455
|
+
...(note ? { note } : {}),
|
|
456
|
+
});
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
/** 점수 후킹 (1~5) */
|
package/index.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export interface CxHook {
|
|
|
75
75
|
/** 채택 기록 폐기 — 문의 전환 시 */
|
|
76
76
|
clearAdopted(): void;
|
|
77
77
|
readonly adoptedAnswerId: number | string | null;
|
|
78
|
+
/** 다안 (0.8.0) — 데코레이트 완료본, [0]=본안. */
|
|
79
|
+
readonly candidates: Array<{ variant: string; label: string; answer: string;
|
|
80
|
+
evidence_keys: string[]; contract_warnings: string[] }>;
|
|
81
|
+
readonly chosenVariant: string;
|
|
82
|
+
/** 후보를 초안 정본으로 (0.8.0). 잘못된 index 는 무동작. */
|
|
83
|
+
choose(index: number): void;
|
|
78
84
|
|
|
79
85
|
/** 발송됨 — 채택본이었을 때만 gold 쌍 후킹. 보낸 뒤 채택 기록을 비운다. */
|
|
80
86
|
answerSent(finalText: string, agent?: string): void;
|
package/locales.js
CHANGED
|
@@ -29,6 +29,9 @@ const en = {
|
|
|
29
29
|
ui_request: "Suggest a reply",
|
|
30
30
|
ui_requesting: "Generating…",
|
|
31
31
|
ui_adopt: "Insert into editor",
|
|
32
|
+
ui_candidates: "AI draft options",
|
|
33
|
+
ui_candidate_use: "Use this draft",
|
|
34
|
+
ui_candidate_chosen: "Selected",
|
|
32
35
|
ui_feedback_ask: "Was this draft helpful?",
|
|
33
36
|
ui_feedback_up: "Helpful",
|
|
34
37
|
ui_feedback_down: "Not helpful",
|
|
@@ -94,6 +97,9 @@ const ko = {
|
|
|
94
97
|
ui_request: "AI 답변 제안",
|
|
95
98
|
ui_requesting: "제안 생성 중...",
|
|
96
99
|
ui_adopt: "에디터에 넣기",
|
|
100
|
+
ui_candidates: "AI 답변 후보",
|
|
101
|
+
ui_candidate_use: "이 안 쓰기",
|
|
102
|
+
ui_candidate_chosen: "선택됨",
|
|
97
103
|
ui_feedback_ask: "이 초안, 쓸 만했나요?",
|
|
98
104
|
ui_feedback_up: "좋아요",
|
|
99
105
|
ui_feedback_down: "싫어요",
|
|
@@ -154,6 +160,9 @@ const ja = {
|
|
|
154
160
|
ui_request: "AI 返信案",
|
|
155
161
|
ui_requesting: "生成中...",
|
|
156
162
|
ui_adopt: "エディタに挿入",
|
|
163
|
+
ui_candidates: "AI回答の候補",
|
|
164
|
+
ui_candidate_use: "この案を使う",
|
|
165
|
+
ui_candidate_chosen: "選択中",
|
|
157
166
|
ui_feedback_ask: "この下書きは役に立ちましたか?",
|
|
158
167
|
ui_feedback_up: "良い",
|
|
159
168
|
ui_feedback_down: "悪い",
|
|
@@ -213,6 +222,9 @@ const ja = {
|
|
|
213
222
|
const zhTW = {
|
|
214
223
|
ui_request: "AI 回覆建議",
|
|
215
224
|
ui_requesting: "產生中...",
|
|
225
|
+
ui_candidates: "AI 回覆候選",
|
|
226
|
+
ui_candidate_use: "使用此草稿",
|
|
227
|
+
ui_candidate_chosen: "已選擇",
|
|
216
228
|
ui_adopt: "插入編輯器",
|
|
217
229
|
ui_feedback_ask: "這份草稿有幫助嗎?",
|
|
218
230
|
ui_feedback_up: "有幫助",
|
package/package.json
CHANGED
package/react.d.ts
CHANGED
|
@@ -41,3 +41,12 @@ export interface AiFeedbackBarProps {
|
|
|
41
41
|
export declare function AiFeedbackBar(props: AiFeedbackBarProps): ReactElement | null;
|
|
42
42
|
|
|
43
43
|
export default AiSuggestPanel;
|
|
44
|
+
|
|
45
|
+
/** 다안 피커 (0.8.0) — 후보 2안 미만이면 null 렌더. 선택 정본은 세션(choose). */
|
|
46
|
+
export declare function CandidatePicker(props: {
|
|
47
|
+
candidates: Array<{ variant: string; label: string; answer: string }>;
|
|
48
|
+
chosen: string;
|
|
49
|
+
messages: Record<string, string>;
|
|
50
|
+
onChoose: (index: number) => void;
|
|
51
|
+
className?: string;
|
|
52
|
+
}): unknown;
|
package/react.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { createElement as h, useCallback, useEffect, useRef, useState } from "react";
|
|
21
21
|
|
|
22
|
-
import { actionOffersTree, aiSuggestTree, aiSuggestView, CLS, feedbackBarTree } from "./view.js";
|
|
22
|
+
import { actionOffersTree, aiSuggestTree, aiSuggestView, candidatePickerTree, CLS, feedbackBarTree } from "./view.js";
|
|
23
23
|
|
|
24
24
|
// 표시 판단은 프레임워크 중립 코어가 갖는다 — Vue·웹 컴포넌트와 같은 것을 쓴다.
|
|
25
25
|
export { aiSuggestView };
|
|
@@ -330,3 +330,37 @@ export function AiFeedbackBar({ session, agent, actorClaimed, className }) {
|
|
|
330
330
|
onClick: (event) => event.stopPropagation() },
|
|
331
331
|
tree.children.map((n, i) => render(n, i)));
|
|
332
332
|
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// ── 다안 피커 (0.8.0) ────────────────────────────────────────────────────────
|
|
336
|
+
//
|
|
337
|
+
// ```jsx
|
|
338
|
+
// <CandidatePicker candidates={session.candidates} chosen={session.chosenVariant}
|
|
339
|
+
// messages={cxMessages} onChoose={(i) => session.choose(i)} />
|
|
340
|
+
// ```
|
|
341
|
+
// 표시 판단은 view.candidatePickerTree 가 소유한다 — 2안 미만이면 아무것도
|
|
342
|
+
// 그리지 않는다. 선택은 세션(choose)이 정본이라 이 컴포넌트는 상태가 없다.
|
|
343
|
+
|
|
344
|
+
export function CandidatePicker({ candidates, chosen, messages, onChoose, className }) {
|
|
345
|
+
const tree = candidatePickerTree({ candidates, chosen, messages });
|
|
346
|
+
if (!tree) return null;
|
|
347
|
+
const render = (node, key) => {
|
|
348
|
+
const props = { key, className: node.cls };
|
|
349
|
+
if (node.tag === "button") {
|
|
350
|
+
props.type = node.type || "button";
|
|
351
|
+
if (node.action === "choose" && onChoose) {
|
|
352
|
+
props.onClick = () => onChoose(node.index);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const children = node.children
|
|
356
|
+
? node.children.map((c, i) => render(c, i))
|
|
357
|
+
: node.text;
|
|
358
|
+
return h(node.tag, props, children);
|
|
359
|
+
};
|
|
360
|
+
return h(
|
|
361
|
+
"div",
|
|
362
|
+
{ className: className ? `${tree.cls} ${className}` : tree.cls,
|
|
363
|
+
onClick: (event) => event.stopPropagation() }, // 문의 행 선택 토글 방지 (패널 선례)
|
|
364
|
+
tree.children.map((c, i) => render(c, i)),
|
|
365
|
+
);
|
|
366
|
+
}
|
package/session.js
CHANGED
|
@@ -166,6 +166,9 @@ export class InquirySession {
|
|
|
166
166
|
this._locks = {}; // action_key\x00target_key → 잠금 조회 결과 (세션 캐시)
|
|
167
167
|
this._verify = {}; // offer_id → 대상 확정(더블체크) 결과 (T2.7, 화면 표시용 캐시)
|
|
168
168
|
|
|
169
|
+
this._candidates = []; // 다안 (0.8.0) — 데코레이트 완료본, [0]=본안
|
|
170
|
+
this._chosenVariant = "primary";
|
|
171
|
+
|
|
169
172
|
// 영속 복원 — TTL 만료분은 store 가 이미 걸렀다
|
|
170
173
|
if (this._id && deps.store) {
|
|
171
174
|
const rec = deps.store.load(this._id);
|
|
@@ -174,6 +177,8 @@ export class InquirySession {
|
|
|
174
177
|
this._answerId = rec.answerId ?? null;
|
|
175
178
|
this._state = rec.state === "declined" ? "declined"
|
|
176
179
|
: (this._draft || this._answerId ? "drafted" : "idle");
|
|
180
|
+
this._candidates = Array.isArray(rec.candidates) ? rec.candidates : [];
|
|
181
|
+
this._chosenVariant = rec.chosenVariant || "primary";
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -358,6 +363,26 @@ export class InquirySession {
|
|
|
358
363
|
get state() { return this._state; }
|
|
359
364
|
get draft() { return this._draft; }
|
|
360
365
|
get adoptedAnswerId() { return this._answerId; }
|
|
366
|
+
get candidates() { return this._candidates.slice(); }
|
|
367
|
+
get chosenVariant() { return this._chosenVariant; }
|
|
368
|
+
|
|
369
|
+
/** 다안 선택 (0.8.0) — 후보를 초안 정본으로. 판단 기록은 없다(고르기는 판단 전 단계). */
|
|
370
|
+
choose(index) {
|
|
371
|
+
const candidate = this._candidates[index];
|
|
372
|
+
if (!candidate) return;
|
|
373
|
+
this._draft = String(candidate.answer || "");
|
|
374
|
+
this._chosenVariant = String(candidate.variant || "primary");
|
|
375
|
+
this._setUiDraft(this._draft);
|
|
376
|
+
this._setUiCandidates();
|
|
377
|
+
this._persist();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_setUiCandidates() {
|
|
381
|
+
// 선택 메서드 — 없는 sink(구 소비처)는 조용히 넘어간다 (0.7.x 호환)
|
|
382
|
+
if (this._ui && typeof this._ui.setCandidates === "function") {
|
|
383
|
+
this._ui.setCandidates(this._candidates.slice(), this._chosenVariant);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
361
386
|
|
|
362
387
|
/** UI sink 연결 — 초안을 자동 주입하지 않는다 (복원은 restore() 명시 호출).
|
|
363
388
|
* @param {{getDraft?:()=>string, setDraft:(t:string)=>void,
|
|
@@ -378,6 +403,7 @@ export class InquirySession {
|
|
|
378
403
|
if (this._ui) {
|
|
379
404
|
this._ui.setDraft(this._draft);
|
|
380
405
|
if (this._ui.setStatus) this._ui.setStatus(this._statusText);
|
|
406
|
+
this._setUiCandidates();
|
|
381
407
|
}
|
|
382
408
|
return { draft: this._draft, answerId: this._answerId, state: this._state };
|
|
383
409
|
}
|
|
@@ -393,6 +419,7 @@ export class InquirySession {
|
|
|
393
419
|
if (this._id && this._d.store) {
|
|
394
420
|
this._d.store.save(this._id, {
|
|
395
421
|
draft: this._draft, answerId: this._answerId, state: this._state,
|
|
422
|
+
candidates: this._candidates, chosenVariant: this._chosenVariant,
|
|
396
423
|
});
|
|
397
424
|
}
|
|
398
425
|
}
|
|
@@ -547,6 +574,12 @@ export class InquirySession {
|
|
|
547
574
|
d.client.discarded(supersededId, "", "redraft");
|
|
548
575
|
}
|
|
549
576
|
this._answerId = newId;
|
|
577
|
+
// 다안 (0.8.0) — 지금 데코레이트해 둔다: 이름은 compose 순간의 재료라
|
|
578
|
+
// 나중에 choose() 가 다시 만들 수 없다. 서버가 후보를 안 보내면 [].
|
|
579
|
+
this._candidates = (Array.isArray(result.candidates) ? result.candidates : [])
|
|
580
|
+
.map((c) => ({ ...c, answer: decorate(String(c.answer || "")) }));
|
|
581
|
+
this._chosenVariant = "primary";
|
|
582
|
+
this._setUiCandidates();
|
|
550
583
|
this._state = "drafted";
|
|
551
584
|
this._setStatus("");
|
|
552
585
|
this._persist();
|
|
@@ -556,6 +589,10 @@ export class InquirySession {
|
|
|
556
589
|
this._draft = supersededDraft;
|
|
557
590
|
this._answerId = supersededId;
|
|
558
591
|
this._state = supersededDraft || supersededId ? "drafted" : "declined";
|
|
592
|
+
// 낡은 후보 세트가 새 실패 위에 남으면 화면이 거짓을 고른다 — 비운다 (0.8.0)
|
|
593
|
+
this._candidates = [];
|
|
594
|
+
this._chosenVariant = "primary";
|
|
595
|
+
this._setUiCandidates();
|
|
559
596
|
this._setUiDraft(supersededDraft);
|
|
560
597
|
this._setStatus(d.textOf(result.declinedReason || "unknown"));
|
|
561
598
|
this._persist();
|
|
@@ -596,15 +633,22 @@ export class InquirySession {
|
|
|
596
633
|
this._draft = "";
|
|
597
634
|
this._statusText = "";
|
|
598
635
|
this._state = "idle";
|
|
636
|
+
this._candidates = []; // 다안도 초안과 같은 수명 (0.8.0)
|
|
637
|
+
this._chosenVariant = "primary";
|
|
638
|
+
this._setUiCandidates();
|
|
599
639
|
this._clearPersist(); // 발송·폐기된 초안을 브라우저에 남기지 않는다
|
|
600
640
|
}
|
|
601
641
|
if (!this._d.client || !id) return;
|
|
602
642
|
send(id);
|
|
603
643
|
}
|
|
604
644
|
|
|
605
|
-
/** 발송 — 채택본이었다면 gold 쌍 후킹, 마지막 판단이라 기록을
|
|
645
|
+
/** 발송 — 채택본이었다면 gold 쌍 후킹, 마지막 판단이라 기록을 비운다.
|
|
646
|
+
* 다안(0.8.0): 본안이 아닌 후보를 골라 보냈으면 note 에 `variant:<id>` 가 남는다. */
|
|
606
647
|
answerSent(finalText, agent) {
|
|
607
|
-
|
|
648
|
+
const variant = this._chosenVariant;
|
|
649
|
+
this._emit((id) => this._d.client.sent(
|
|
650
|
+
id, finalText, agent, variant !== "primary" ? `variant:${variant}` : "",
|
|
651
|
+
), true);
|
|
608
652
|
}
|
|
609
653
|
|
|
610
654
|
/** 품질 점수 1~5 — 발송 전 여러 번 가능 (비소모). 사유는 여기 안 실린다(아래 계약). */
|
package/view.d.ts
CHANGED
|
@@ -84,3 +84,11 @@ export declare function feedbackBarView(input: {
|
|
|
84
84
|
} | null;
|
|
85
85
|
/** action: "up" | "down" — 어댑터가 자기 이벤트로 배선한다 */
|
|
86
86
|
export declare function feedbackBarTree(input: Parameters<typeof feedbackBarView>[0]): AiSuggestNode | null;
|
|
87
|
+
|
|
88
|
+
export declare const CAND_CLS: Record<string, string>;
|
|
89
|
+
/** 다안 피커 트리 (0.8.0) — 2안 미만이면 null. 노드의 index 는 choose(index) 배선용. */
|
|
90
|
+
export declare function candidatePickerTree(input: {
|
|
91
|
+
candidates: Array<{ variant: string; label: string; answer: string }>;
|
|
92
|
+
chosen?: string;
|
|
93
|
+
messages?: Record<string, string>;
|
|
94
|
+
}): object | null;
|
package/view.js
CHANGED
|
@@ -419,3 +419,49 @@ export function feedbackBarTree(input) {
|
|
|
419
419
|
],
|
|
420
420
|
};
|
|
421
421
|
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
// ── 다안 피커 (0.8.0) — 후보 2~3안 중 하나를 초안으로 ──────────────────────
|
|
425
|
+
export const CAND_CLS = {
|
|
426
|
+
root: "fcx-cand",
|
|
427
|
+
item: "fcx-cand-item",
|
|
428
|
+
label: "fcx-cand-label",
|
|
429
|
+
excerpt: "fcx-cand-excerpt",
|
|
430
|
+
meta: "fcx-cand-meta",
|
|
431
|
+
use: "fcx-cand-use",
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const CAND_EXCERPT_CHARS = 160;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 후보 목록의 프레임워크 중립 트리. 후보가 2안 미만이면 null — 피커는
|
|
438
|
+
* "고를 것이 있을 때"만 선다 (빈 상자 금지, aiSuggestTree 와 같은 규율).
|
|
439
|
+
* 노드에 `index` 가 있으면 어댑터가 choose(index) 로 배선한다.
|
|
440
|
+
*/
|
|
441
|
+
export function candidatePickerTree({ candidates, chosen, messages } = {}) {
|
|
442
|
+
const list = Array.isArray(candidates) ? candidates : [];
|
|
443
|
+
if (list.length < 2) return null;
|
|
444
|
+
const m = messages || {};
|
|
445
|
+
return {
|
|
446
|
+
tag: "div", cls: CAND_CLS.root,
|
|
447
|
+
children: list.map((c, index) => {
|
|
448
|
+
const isChosen = String(c.variant || "") === String(chosen || "primary");
|
|
449
|
+
return {
|
|
450
|
+
tag: "div",
|
|
451
|
+
cls: isChosen ? `${CAND_CLS.item} is-chosen` : CAND_CLS.item,
|
|
452
|
+
children: [
|
|
453
|
+
{ tag: "span", cls: CAND_CLS.label,
|
|
454
|
+
text: String(c.label || c.variant || "") },
|
|
455
|
+
{ tag: "div", cls: CAND_CLS.excerpt,
|
|
456
|
+
text: String(c.answer || "").slice(0, CAND_EXCERPT_CHARS) },
|
|
457
|
+
{ tag: "span", cls: CAND_CLS.meta,
|
|
458
|
+
text: isChosen ? (m.ui_candidate_chosen || "") : "" },
|
|
459
|
+
...(isChosen ? [] : [{
|
|
460
|
+
tag: "button", cls: CAND_CLS.use, type: "button", disabled: false,
|
|
461
|
+
text: m.ui_candidate_use || "", action: "choose", index,
|
|
462
|
+
}]),
|
|
463
|
+
],
|
|
464
|
+
};
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
}
|