@fcg-labs/cx-agent-hook 0.1.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/view.js ADDED
@@ -0,0 +1,60 @@
1
+ /** 표시 판단과 클래스 이름 — 프레임워크 중립.
2
+ *
3
+ * React·Vue·웹 컴포넌트가 **같은 판단**을 쓴다. 여기가 갈리면 프레임워크마다
4
+ * 다른 제품이 되고, 어느 하나를 고쳐도 나머지는 그대로 남는다.
5
+ *
6
+ * 렌더러 없이 검증된다 — 이 패키지는 react-dom 도 vue 도 의존하지 않는다.
7
+ */
8
+
9
+ /** 지금 무엇을 그릴지 — React 없이 검증 가능한 순수 함수.
10
+ *
11
+ * 화면 계층을 얇게 두는 이유: 이 판단이 곧 제품 행동이고, 렌더러 없이도
12
+ * 검증돼야 한다. (SDK 는 react-dom 을 의존하지 않는다.)
13
+ *
14
+ * 문구는 인자로 받는다 — 이 함수가 언어를 알면 로케일이 두 곳에 생긴다.
15
+ * 정본은 setup 에서 정해져 `hook.messages` 에 들어 있다.
16
+ *
17
+ * @param {"idle"|"loading"|"done"} state
18
+ * @param {object|null} result requestAnswer 결과
19
+ * @param {(reason: string) => string} declineText
20
+ * @param {Record<string,string>} messages hook.messages
21
+ */
22
+ export function aiSuggestView({ state, result, declineText, messages = {} }) {
23
+ const loading = state === "loading";
24
+ const done = state === "done" && Boolean(result);
25
+ const answered = done && Boolean(result.answered);
26
+
27
+ let body = { kind: "none" };
28
+ if (answered) {
29
+ body = {
30
+ kind: "answer",
31
+ text: result.answer || "",
32
+ // 근거 제목이 없으면 키로 대신한다 — 빈 칩을 그리지 않는다.
33
+ evidence: (result.evidence || [])
34
+ .map((e) => e.title || e.unit_key)
35
+ .filter(Boolean),
36
+ evidenceLabel: messages.ui_evidence,
37
+ };
38
+ } else if (done) {
39
+ body = { kind: "declined", text: declineText(result.declinedReason) };
40
+ }
41
+
42
+ return {
43
+ buttonLabel: loading ? messages.ui_requesting : messages.ui_request,
44
+ buttonDisabled: loading,
45
+ showAdopt: answered,
46
+ adoptLabel: messages.ui_adopt,
47
+ body,
48
+ };
49
+ }
50
+
51
+ export const CLS = {
52
+ root: "fcx-ai",
53
+ head: "fcx-ai-head",
54
+ button: "fcx-ai-button",
55
+ adopt: "fcx-ai-adopt",
56
+ body: "fcx-ai-body",
57
+ answer: "fcx-ai-answer",
58
+ evidence: "fcx-ai-evidence",
59
+ declined: "fcx-ai-declined",
60
+ };
package/vue.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { DefineComponent } from "vue";
2
+ import type { CxHook } from "./index.js";
3
+
4
+ export { aiSuggestView } from "./view.js";
5
+ export type { AiSuggestView } from "./view.js";
6
+
7
+ export interface AiSuggestPanelProps {
8
+ hook: CxHook;
9
+ inquiry?: string;
10
+ /** 바깥 배치용 (레이아웃만 — 색·간격은 styles.css 변수로) */
11
+ className?: string;
12
+ }
13
+
14
+ /** `@adopt` 로 채택 문구를 emit 한다. hook 이 미설정이면 아무것도 그리지 않는다. */
15
+ export declare const AiSuggestPanel: DefineComponent<AiSuggestPanelProps>;
16
+ export default AiSuggestPanel;
package/vue.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * AI 답변 제안 패널 — Vue 3.
3
+ *
4
+ * ```vue
5
+ * <script setup>
6
+ * import { AiSuggestPanel } from "@fcg-labs/cx-agent-hook/vue";
7
+ * import "@fcg-labs/cx-agent-hook/styles.css";
8
+ * import { cxHook } from "./cxAgentHook";
9
+ * </script>
10
+ *
11
+ * <template>
12
+ * <AiSuggestPanel :hook="cxHook" :inquiry="selected?.content" @adopt="setAnswerText" />
13
+ * </template>
14
+ * ```
15
+ *
16
+ * 표시 판단은 `view.js` 가 갖는다 — React·웹 컴포넌트와 **같은 것**을 쓴다.
17
+ * 여기서 다시 판단하면 프레임워크마다 다른 제품이 된다.
18
+ *
19
+ * JSX·SFC 를 쓰지 않는 이유는 react.js 와 같다: 이 패키지에는 빌드 단계가 없다.
20
+ * Vue 는 optional peer 다 (>=3.0).
21
+ */
22
+ import { computed, defineComponent, h, ref, watch } from "vue";
23
+
24
+ import { aiSuggestView, CLS } from "./view.js";
25
+
26
+ export { aiSuggestView };
27
+
28
+ export const AiSuggestPanel = defineComponent({
29
+ name: "AiSuggestPanel",
30
+ props: {
31
+ hook: { type: Object, required: true },
32
+ inquiry: { type: String, default: "" },
33
+ /** 바깥 배치용 (레이아웃만 — 색·간격은 styles.css 변수로) */
34
+ className: { type: String, default: "" },
35
+ },
36
+ emits: ["adopt"],
37
+ setup(props, { emit }) {
38
+ const state = ref("idle");
39
+ const result = ref(null);
40
+
41
+ // 다른 문의로 옮기면 앞 문의의 제안이 남아 있으면 안 된다. 채택 기록도 같이
42
+ // 지운다 — 안 지우면 다음 문의를 발송할 때 엉뚱한 answer_id 로 후킹된다.
43
+ watch(
44
+ () => props.inquiry,
45
+ () => {
46
+ state.value = "idle";
47
+ result.value = null;
48
+ props.hook.clearAdopted();
49
+ },
50
+ { immediate: true },
51
+ );
52
+
53
+ const request = async () => {
54
+ if (!props.inquiry || state.value === "loading") return;
55
+ state.value = "loading";
56
+ result.value = await props.hook.requestAnswer(props.inquiry);
57
+ state.value = "done";
58
+ };
59
+
60
+ const adopt = () => {
61
+ const r = result.value;
62
+ if (!r || !r.answered) return;
63
+ props.hook.noteAdopted(r.answerId);
64
+ emit("adopt", r.answer);
65
+ };
66
+
67
+ const view = computed(() =>
68
+ aiSuggestView({
69
+ state: state.value,
70
+ result: result.value,
71
+ declineText: props.hook.declineText,
72
+ messages: props.hook.messages,
73
+ }),
74
+ );
75
+
76
+ return () => {
77
+ if (!props.hook || !props.hook.enabled) return null;
78
+ const v = view.value;
79
+ return h(
80
+ "div",
81
+ {
82
+ class: props.className ? `${CLS.root} ${props.className}` : CLS.root,
83
+ // 이 패널은 보통 "클릭하면 선택되는" 문의 행 안에 놓인다. 버튼을
84
+ // 누르려다 행 선택이 토글되면 제안이 초기화된다.
85
+ onClick: (event) => event.stopPropagation(),
86
+ },
87
+ [
88
+ h("div", { class: CLS.head }, [
89
+ h(
90
+ "button",
91
+ { type: "button", class: CLS.button, disabled: v.buttonDisabled, onClick: request },
92
+ v.buttonLabel,
93
+ ),
94
+ v.showAdopt
95
+ ? h("button", { type: "button", class: CLS.adopt, onClick: adopt }, v.adoptLabel)
96
+ : null,
97
+ ]),
98
+ v.body.kind === "answer"
99
+ ? h("div", { class: CLS.body }, [
100
+ h("div", { class: CLS.answer }, v.body.text),
101
+ v.body.evidence.length > 0
102
+ ? h(
103
+ "div",
104
+ { class: CLS.evidence },
105
+ `${v.body.evidenceLabel}: ${v.body.evidence.join(" · ")}`,
106
+ )
107
+ : null,
108
+ ])
109
+ : null,
110
+ v.body.kind === "declined"
111
+ ? h("div", { class: CLS.declined }, v.body.text)
112
+ : null,
113
+ ],
114
+ );
115
+ };
116
+ },
117
+ });
118
+
119
+ export default AiSuggestPanel;