@vibexnpm/talkx 7.0.0 → 7.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -142,6 +142,7 @@ class ChatClient extends EventEmitter {
142
142
  THINKING: '생각 중…',
143
143
  SEARCHING: '검색 중…',
144
144
  PLANNING: '기획 중…',
145
+ DOCUMENT_ANALYSIS: '문서 분석 중…',
145
146
  WRITING: '작성 중…'
146
147
  };
147
148
  // PHASE 의 expiresAt 누락/이상 시 fallback TTL (ms) — 서버 ttl(최소 30s) 보다 약간 길게.
@@ -3078,12 +3079,19 @@ class ChatClient extends EventEmitter {
3078
3079
  return;
3079
3080
  }
3080
3081
  const phase = event.phase || null;
3082
+ const samePhase = Boolean(prevSlot && prevSlot.phase === phase);
3083
+ // 같은 phase의 일반 heartbeat에는 progress가 없다. 그 틱이 직전 실측 숫자를 지우지 않게
3084
+ // 보존하고, phase가 바뀔 때만 이전 작업의 숫자를 폐기한다.
3085
+ const progress = event.progress
3086
+ ? this._normalizeAssistantProgress(event.progress)
3087
+ : (samePhase && prevSlot ? prevSlot.progress : null);
3081
3088
  // 같은 stream 의 PHASE 재발행(B-3 synthesis heartbeat 등)이면 누적 상태를 보존한다.
3082
3089
  // ⚠️ 새 stream 의 커서는 <b>-1</b> 로 연다. DeltaCoalescer 의 첫 청크가 seq=0 이라 0 으로 열면
3083
3090
  // 첫 글자가 "중복"으로 버려진다 (설계 §5.5.0).
3084
3091
  const prev = prevSlot;
3085
3092
  const slot = this._setAssistantSlot(roomId, streamId, event, {
3086
3093
  phase,
3094
+ progress,
3087
3095
  phaseUpdatedAt: createdAt !== null ? createdAt : (prev ? prev.phaseUpdatedAt : 0),
3088
3096
  text: prev ? (prev.text || '') : '',
3089
3097
  lastSeq: prev ? this._seqOf(prev.lastSeq) : -1,
@@ -3132,7 +3140,9 @@ class ChatClient extends EventEmitter {
3132
3140
  lastSeq = seq;
3133
3141
  }
3134
3142
 
3135
- const slot = this._setAssistantSlot(roomId, streamId, event, { phase, text, lastSeq, textAvailable });
3143
+ const slot = this._setAssistantSlot(roomId, streamId, event, {
3144
+ phase, progress: prev ? (prev.progress || null) : null, text, lastSeq, textAvailable
3145
+ });
3136
3146
  this._emitAssistantStream(roomId, streamId, slot, {
3137
3147
  active: true,
3138
3148
  text: slot.textAvailable ? slot.text : null, // 누적 전체(버블 렌더용)
@@ -3153,6 +3163,7 @@ class ChatClient extends EventEmitter {
3153
3163
  active: false,
3154
3164
  personaId: event.personaId || (slot && slot.personaId) || null,
3155
3165
  phase: null,
3166
+ progress: null,
3156
3167
  label: null,
3157
3168
  status: event.status || null,
3158
3169
  messageId: event.messageId || null
@@ -3242,7 +3253,7 @@ class ChatClient extends EventEmitter {
3242
3253
  // 유예 구간에 걸린 종료분. 진행 표시를 켜지 않고 최종 메시지로 바로 확정시킨다.
3243
3254
  this._tombstoneAssistantStream(roomId, streamId);
3244
3255
  this._emitAssistantStream(roomId, streamId, null, {
3245
- active: false, phase: null, label: null, personaId: s.personaId || null,
3256
+ active: false, phase: null, progress: null, label: null, personaId: s.personaId || null,
3246
3257
  status: s.doneStatus || null, messageId: s.messageId || null, resumed: true
3247
3258
  });
3248
3259
  return;
@@ -3253,6 +3264,7 @@ class ChatClient extends EventEmitter {
3253
3264
  personaId: s.personaId, expiresAt: undefined
3254
3265
  }, {
3255
3266
  phase: s.phase || null,
3267
+ progress: this._normalizeAssistantProgress(s.progress),
3256
3268
  phaseUpdatedAt: typeof s.phaseUpdatedAt === 'number' ? s.phaseUpdatedAt : 0,
3257
3269
  text: textAvailable ? (s.text || '') : '',
3258
3270
  lastSeq: this._seqOf(s.lastSeq),
@@ -3452,6 +3464,7 @@ class ChatClient extends EventEmitter {
3452
3464
  const slot = {
3453
3465
  personaId: event.personaId || (prev && prev.personaId) || null,
3454
3466
  phase: fields.phase,
3467
+ progress: fields.progress || null,
3455
3468
  // 마지막으로 <b>적용된</b> PHASE 의 발행 시각 — pending PHASE 역행 판정 커서(§6.2).
3456
3469
  phaseUpdatedAt: fields.phaseUpdatedAt !== undefined
3457
3470
  ? fields.phaseUpdatedAt
@@ -3476,7 +3489,7 @@ class ChatClient extends EventEmitter {
3476
3489
  this._deleteAssistantSlot(roomId, streamId);
3477
3490
  this.logger.debug(`Assistant progress auto-cleared (timeout): room=${roomId}, stream=${streamId}`);
3478
3491
  this._emitAssistantStream(roomId, streamId, slot, {
3479
- active: false, phase: null, label: null, reason: 'timeout'
3492
+ active: false, phase: null, progress: null, label: null, reason: 'timeout'
3480
3493
  });
3481
3494
  }
3482
3495
 
@@ -3491,12 +3504,14 @@ class ChatClient extends EventEmitter {
3491
3504
  */
3492
3505
  _emitAssistantStream(roomId, streamId, slot, overrides) {
3493
3506
  const phase = 'phase' in overrides ? overrides.phase : (slot ? slot.phase : null);
3507
+ const progress = 'progress' in overrides ? overrides.progress : (slot ? slot.progress : null);
3494
3508
  this.emit('assistantProgress', {
3495
3509
  roomId,
3496
3510
  streamId,
3497
3511
  personaId: slot ? slot.personaId : null,
3498
3512
  phase,
3499
- label: phase ? (this._assistantPhaseLabels[phase] || null) : null,
3513
+ progress,
3514
+ label: this._assistantPhaseLabel(phase, progress),
3500
3515
  textAvailable: slot ? slot.textAvailable : true,
3501
3516
  lastSeq: slot ? slot.lastSeq : -1,
3502
3517
  ...overrides
@@ -3528,6 +3543,7 @@ class ChatClient extends EventEmitter {
3528
3543
  return {
3529
3544
  roomId, active: false, streamCount: 0,
3530
3545
  streamId: null, personaId: null, phase: null, label: null, text: null, textAvailable: true,
3546
+ progress: null,
3531
3547
  streams: []
3532
3548
  };
3533
3549
  }
@@ -3539,7 +3555,8 @@ class ChatClient extends EventEmitter {
3539
3555
  streamId,
3540
3556
  personaId: slot.personaId,
3541
3557
  phase: slot.phase,
3542
- label: slot.phase ? (this._assistantPhaseLabels[slot.phase] || null) : null,
3558
+ progress: slot.progress || null,
3559
+ label: this._assistantPhaseLabel(slot.phase, slot.progress),
3543
3560
  text: slot.textAvailable ? (slot.text || null) : null,
3544
3561
  textAvailable: slot.textAvailable,
3545
3562
  // stream 단위 UI(권장 (B))가 마운트 즉시 버블을 복원할 수 있도록 전부 싣는다.
@@ -3550,7 +3567,8 @@ class ChatClient extends EventEmitter {
3550
3567
  streamId: id,
3551
3568
  personaId: s.personaId,
3552
3569
  phase: s.phase,
3553
- label: s.phase ? (this._assistantPhaseLabels[s.phase] || null) : null,
3570
+ progress: s.progress || null,
3571
+ label: this._assistantPhaseLabel(s.phase, s.progress),
3554
3572
  text: s.textAvailable ? (s.text || null) : null,
3555
3573
  textAvailable: s.textAvailable,
3556
3574
  resumed: s.resumed === true
@@ -3558,6 +3576,32 @@ class ChatClient extends EventEmitter {
3558
3576
  };
3559
3577
  }
3560
3578
 
3579
+ /** @private */
3580
+ _normalizeAssistantProgress(progress) {
3581
+ if (!progress || !['ANALYZING', 'SUMMARIZING'].includes(progress.stage)) return null;
3582
+ if (progress.stage === 'SUMMARIZING') {
3583
+ return { stage: 'SUMMARIZING', current: null, total: null, unit: null };
3584
+ }
3585
+ const current = Number.isInteger(progress.current) ? progress.current : null;
3586
+ const total = Number.isInteger(progress.total) ? progress.total : null;
3587
+ const unit = ['DOCUMENT', 'PAGE', 'TEXT_CHUNK'].includes(progress.unit) ? progress.unit : null;
3588
+ if (current === null || total === null || !unit || current < 0 || total < 1 || current > total) {
3589
+ return null;
3590
+ }
3591
+ return { stage: 'ANALYZING', current, total, unit };
3592
+ }
3593
+
3594
+ /** @private */
3595
+ _assistantPhaseLabel(phase, progress) {
3596
+ if (!phase) return null;
3597
+ if (phase !== 'DOCUMENT_ANALYSIS') return this._assistantPhaseLabels[phase] || null;
3598
+ if (progress && progress.stage === 'SUMMARIZING') return '분석 결과 종합 중…';
3599
+ if (!progress || progress.stage !== 'ANALYZING') return this._assistantPhaseLabels[phase];
3600
+ const suffix = progress.unit === 'PAGE' ? '페이지'
3601
+ : progress.unit === 'TEXT_CHUNK' ? '구간' : '개';
3602
+ return `문서 분석 중… (${progress.current}/${progress.total}${suffix})`;
3603
+ }
3604
+
3561
3605
  /**
3562
3606
  * 방의 <b>현재</b> AI 진행 상태를 즉시 반환 (네트워크 호출 없음).
3563
3607
  *
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 일정 제안 카드 타입 — 소비자 코드가 <b>실제로 컴파일되는지</b>.
3
+ *
4
+ * <p>jest 는 `.d.ts` 를 보지 않는다. 카드 렌더러가 쓰는 모양(모르는 kind 폴백 · REJECTED 에서만 읽는 사유 코드 ·
5
+ * 항상 있는 공개 범위 고지)이 선언과 맞는지 여기서 tsc 로 고정한다.</p>
6
+ *
7
+ * <p>실행: {@code npm run typecheck}</p>
8
+ */
9
+ import type { ChatMessage, ScheduleCard } from '../index';
10
+
11
+ declare const message: ChatMessage;
12
+
13
+ // 1) 메시지에는 없을 수 있다 — 일반 메시지는 필드가 부재.
14
+ const maybeCard: ScheduleCard | null | undefined = message.scheduleCard;
15
+
16
+ // 2) 공개 범위 고지는 항상 문자열이다 — 카드를 그리면 반드시 표시한다.
17
+ function notice(card: ScheduleCard): string {
18
+ return card.visibilityNotice;
19
+ }
20
+
21
+ // 3) kind 는 열린 문자열이다 — 알려진 값 분기 + default 텍스트 폴백이 성립해야 한다.
22
+ function headline(card: ScheduleCard, fallbackContent: string | null): string {
23
+ switch (card.kind) {
24
+ case 'PROPOSED': return '일정을 등록할까요?';
25
+ case 'CREATED': return '일정을 등록했습니다';
26
+ case 'REJECTED': return rejectedText(card.failureCode);
27
+ case 'UNKNOWN': return '등록됐는지 확인하지 못했습니다. 캘린더에서 확인해 주세요';
28
+ case 'CANCELLED': return '일정 제안을 취소했습니다';
29
+ case 'EXPIRED': return '만료되었습니다. 다시 요청해 주세요';
30
+ default: return fallbackContent ?? ''; // 모르는 kind — content 로 폴백
31
+ }
32
+ }
33
+
34
+ // 4) 사유 코드는 선택 필드다 — 없거나 모르는 코드는 일반 거절 문구.
35
+ function rejectedText(code: string | undefined): string {
36
+ const known: Record<string, string> = {
37
+ PARTICIPANT_LIMIT_EXCEEDED: '참여자가 너무 많아(최대 100명) 등록하지 못했습니다',
38
+ };
39
+ return (code !== undefined && known[code] !== undefined ? known[code] : '일정을 등록하지 못했습니다')
40
+ + '. 다시 요청해 주세요';
41
+ }
42
+
43
+ // 5) 선택 필드는 null 일 수 있다 — 그대로 비교·병합할 수 있어야 한다.
44
+ function names(card: ScheduleCard): string {
45
+ return (card.participantNames ?? []).join(', ') + (card.title ?? '') + (card.whenText ?? '') + (card.location ?? '');
46
+ }
47
+
48
+ // 6) 알 수 없는 kind 문자열도 받아들인다(구버전 호환).
49
+ const future: ScheduleCard = { proposalId: 'p-1', kind: 'SOMETHING_NEW', visibilityNotice: '이 방 구성원에게 공개되는 일정입니다' };
50
+
51
+ export { maybeCard, notice, headline, names, future };
package/types/index.d.ts CHANGED
@@ -772,6 +772,58 @@ export interface ChatMessage {
772
772
  * 마스킹이 호출자마다 다르게 계산되기 때문이다.</p>
773
773
  */
774
774
  voteCard?: VoteCard | null;
775
+ /**
776
+ * 일정 제안 카드 — PM 이 대화에서 일정을 «제안» 했거나 그 결과를 알리는 메시지의 payload.
777
+ *
778
+ * <p><b>모르면 그냥 텍스트로 그려라 — 그게 정상 동작이다.</b> {@code content} 에 같은 내용과 공개 범위 고지가
779
+ * 사람이 읽는 형태로 들어 있다. 반대로 카드를 그린다면 {@code content} 를 다시 그리지 마라.</p>
780
+ *
781
+ * <p>결과(등록됨·거절·확인 불가·취소·만료)는 원래 카드를 고치지 않고 {@code kind} 가 다른 <b>새 메시지</b>로 온다.
782
+ * 지금 승인할 수 있는지는 카드가 아니라 {@code GET /api/v1/calendar/proposals/{proposalId}} 의
783
+ * {@code approvable} 이 말한다(SDK 메서드 없음 — 다른 캘린더 API 처럼 REST 로 직접 부른다).</p>
784
+ */
785
+ scheduleCard?: ScheduleCard | null;
786
+ }
787
+
788
+ /**
789
+ * 일정 제안 카드 payload — 제안 하나에 «승인할까요?» 카드 한 장, 결과 카드는 최대 한 장이다.
790
+ *
791
+ * <p>값은 발행 시점 <b>스냅샷</b>이다. 결과는 원래 카드를 고치지 않고 {@code kind} 가 다른 새 메시지로 온다.</p>
792
+ */
793
+ export interface ScheduleCard {
794
+ /** 조회·승인·취소의 키. */
795
+ proposalId: string;
796
+ /**
797
+ * {@code 'PROPOSED'}(승인 대기) | {@code 'CREATED'} | {@code 'REJECTED'} | {@code 'UNKNOWN'} |
798
+ * {@code 'CANCELLED'} | {@code 'EXPIRED'}.
799
+ *
800
+ * <p>🔴 {@code 'PROPOSED'} 는 «등록됨» 이 아니다. 🔴 {@code 'UNKNOWN'} 은 «실패» 가 아니라 «등록됐는지 모른다» 다 —
801
+ * 재시도·다시 요청 버튼을 두지 않는다(만들어졌을 수 있어 다시 만들면 일정이 둘 된다).</p>
802
+ *
803
+ * <p>🔴 <b>모르는 값이 올 수 있다</b> — switch 의 default 는 텍스트({@code content}) 폴백으로 둔다.</p>
804
+ */
805
+ kind: 'PROPOSED' | 'CREATED' | 'REJECTED' | 'UNKNOWN' | 'CANCELLED' | 'EXPIRED' | (string & {});
806
+ /** 제목 스냅샷. 비어 있을 수 있다. */
807
+ title?: string | null;
808
+ /** 사람이 읽는 시각 — 서버가 <b>제안 당시 해석 타임존</b>으로 만든 문자열(끝에 존 ID). 다시 계산하지 않는다. */
809
+ whenText?: string | null;
810
+ /** 장소 스냅샷. 비어 있을 수 있다. */
811
+ location?: string | null;
812
+ /** 초대 대상 표시 이름. 비어 있어도 공개 범위는 방 전체다({@link ScheduleCard.visibilityNotice}). */
813
+ participantNames?: string[] | null;
814
+ /**
815
+ * 🔴 공개 범위 고지 — <b>참가자 수와 관계없이 항상</b> 온다. 카드를 그린다면 반드시 표시한다.
816
+ * 초대한 사람이 없어도 이 방 구성원 전원에게 보이는 일정이다.
817
+ */
818
+ visibilityNotice: string;
819
+ /**
820
+ * 거절 사유 <b>코드</b> — 🔴 {@code kind === 'REJECTED'} 일 때만 오고, 그때도 없을 수 있다.
821
+ *
822
+ * <p>문구 변환은 화면이 한다. 코드가 없거나 모르는 코드면 <b>일반 거절 문구</b>
823
+ * («일정을 등록하지 못했습니다») 를 보여주고 «다시 요청해 주세요» 를 붙인다. 상류 원문 메시지는 오지 않는다.
824
+ * 수동 입력 폼의 오류 안내(필드별 표시 등)를 쓰지 않는다 — 제안에는 고칠 폼이 없다.</p>
825
+ */
826
+ failureCode?: string;
775
827
  }
776
828
 
777
829
  /**
@@ -1264,6 +1316,18 @@ export interface TypingEvent {
1264
1316
  senderType: 'USER' | 'ASSISTANT';
1265
1317
  }
1266
1318
 
1319
+ /** 문서 분석의 재입장 복원 가능한 실측 진행률. */
1320
+ export interface DocumentAnalysisProgress {
1321
+ /** ANALYZING=페이지/텍스트 구간 처리, SUMMARIZING=확보한 근거 종합. */
1322
+ stage: 'ANALYZING' | 'SUMMARIZING';
1323
+ /** ANALYZING일 때 처리 완료 수. */
1324
+ current: number | null;
1325
+ /** ANALYZING일 때 전체 수. */
1326
+ total: number | null;
1327
+ /** 진행 단위. SUMMARIZING일 때 null. */
1328
+ unit: 'DOCUMENT' | 'PAGE' | 'TEXT_CHUNK' | null;
1329
+ }
1330
+
1267
1331
  /**
1268
1332
  * AI 진행 표시 이벤트 — `/topic/chat/{roomId}/assistant-stream` 의 PHASE/DONE.
1269
1333
  *
@@ -1281,6 +1345,7 @@ export interface TypingEvent {
1281
1345
  *
1282
1346
  * 현재는 PM_BACKSTAGE(방당 단일 PM) 만 phase 를 발행 — 멘션/버튼/14인 경로는 본 이벤트 없이 typing 만.
1283
1347
  */
1348
+
1284
1349
  export interface AssistantProgressEvent {
1285
1350
  /** 채팅방 ID. */
1286
1351
  roomId: string;
@@ -1294,7 +1359,9 @@ export interface AssistantProgressEvent {
1294
1359
  /** 진행 주체 페르소나 ID. */
1295
1360
  personaId: string | null;
1296
1361
  /** 진행 단계 — `active:true` 일 때. 종료 시 null. */
1297
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1362
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1363
+ /** DOCUMENT_ANALYSIS의 실측 진행률. 같은 phase heartbeat 사이와 재입장 후에도 보존된다. */
1364
+ progress?: DocumentAnalysisProgress | null;
1298
1365
  /** phase 의 기본 표시 라벨(예: "생각 중…"). SDK 매핑 — 커스텀하려면 `phase` 사용. 종료 시 null. */
1299
1366
  label: string | null;
1300
1367
  /** true=진행중(PHASE/DELTA) / false=종료(DONE·TTL). */
@@ -1360,7 +1427,8 @@ export interface AssistantRoomProgressEvent {
1360
1427
  /** 표시 기준이 된 stream (가장 최근 갱신). 없으면 null. */
1361
1428
  streamId: string | null;
1362
1429
  personaId: string | null;
1363
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1430
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1431
+ progress?: DocumentAnalysisProgress | null;
1364
1432
  label: string | null;
1365
1433
  /** 표시 기준 stream 의 누적 본문. `textAvailable:false` 거나 진행 중이 아니면 null. */
1366
1434
  text?: string | null;
@@ -1372,7 +1440,8 @@ export interface AssistantRoomProgressEvent {
1372
1440
  export interface AssistantProgressStreamSnapshot {
1373
1441
  streamId: string;
1374
1442
  personaId: string | null;
1375
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1443
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1444
+ progress?: DocumentAnalysisProgress | null;
1376
1445
  label: string | null;
1377
1446
  text: string | null;
1378
1447
  textAvailable: boolean;