@things-factory/board-ai 10.0.0-beta.64

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.
Files changed (95) hide show
  1. package/client/components/board-ai-chat.test.ts +120 -0
  2. package/client/components/board-ai-chat.ts +1502 -0
  3. package/client/components/chat-input-builder.ts +40 -0
  4. package/client/components/markdown.test.ts +220 -0
  5. package/client/components/markdown.ts +184 -0
  6. package/client/index.ts +11 -0
  7. package/client/tsconfig.json +13 -0
  8. package/client/utils/board-edit-patch.ts +200 -0
  9. package/config/config.development.js +43 -0
  10. package/config/config.production.js +15 -0
  11. package/dist-client/components/board-ai-chat.d.ts +127 -0
  12. package/dist-client/components/board-ai-chat.js +1455 -0
  13. package/dist-client/components/board-ai-chat.js.map +1 -0
  14. package/dist-client/components/board-ai-chat.test.d.ts +1 -0
  15. package/dist-client/components/board-ai-chat.test.js +112 -0
  16. package/dist-client/components/board-ai-chat.test.js.map +1 -0
  17. package/dist-client/components/chat-input-builder.d.ts +30 -0
  18. package/dist-client/components/chat-input-builder.js +25 -0
  19. package/dist-client/components/chat-input-builder.js.map +1 -0
  20. package/dist-client/components/markdown.d.ts +16 -0
  21. package/dist-client/components/markdown.js +167 -0
  22. package/dist-client/components/markdown.js.map +1 -0
  23. package/dist-client/components/markdown.test.d.ts +1 -0
  24. package/dist-client/components/markdown.test.js +187 -0
  25. package/dist-client/components/markdown.test.js.map +1 -0
  26. package/dist-client/index.d.ts +11 -0
  27. package/dist-client/index.js +12 -0
  28. package/dist-client/index.js.map +1 -0
  29. package/dist-client/tsconfig.tsbuildinfo +1 -0
  30. package/dist-client/utils/board-edit-patch.d.ts +73 -0
  31. package/dist-client/utils/board-edit-patch.js +159 -0
  32. package/dist-client/utils/board-edit-patch.js.map +1 -0
  33. package/dist-server/index.d.ts +21 -0
  34. package/dist-server/index.js +25 -0
  35. package/dist-server/index.js.map +1 -0
  36. package/dist-server/service/apply-patch.d.ts +46 -0
  37. package/dist-server/service/apply-patch.js +211 -0
  38. package/dist-server/service/apply-patch.js.map +1 -0
  39. package/dist-server/service/assistant.d.ts +75 -0
  40. package/dist-server/service/assistant.js +1298 -0
  41. package/dist-server/service/assistant.js.map +1 -0
  42. package/dist-server/service/board-ai-resolver.d.ts +40 -0
  43. package/dist-server/service/board-ai-resolver.js +260 -0
  44. package/dist-server/service/board-ai-resolver.js.map +1 -0
  45. package/dist-server/service/chat-message/chat-message.d.ts +24 -0
  46. package/dist-server/service/chat-message/chat-message.js +108 -0
  47. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  48. package/dist-server/service/chat-message/index.d.ts +3 -0
  49. package/dist-server/service/chat-message/index.js +7 -0
  50. package/dist-server/service/chat-message/index.js.map +1 -0
  51. package/dist-server/service/chat-session/chat-session.d.ts +22 -0
  52. package/dist-server/service/chat-session/chat-session.js +109 -0
  53. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  54. package/dist-server/service/chat-session/index.d.ts +3 -0
  55. package/dist-server/service/chat-session/index.js +7 -0
  56. package/dist-server/service/chat-session/index.js.map +1 -0
  57. package/dist-server/service/chat-session-resolver.d.ts +13 -0
  58. package/dist-server/service/chat-session-resolver.js +178 -0
  59. package/dist-server/service/chat-session-resolver.js.map +1 -0
  60. package/dist-server/service/index.d.ts +14 -0
  61. package/dist-server/service/index.js +26 -0
  62. package/dist-server/service/index.js.map +1 -0
  63. package/dist-server/service/patch-entry/index.d.ts +3 -0
  64. package/dist-server/service/patch-entry/index.js +7 -0
  65. package/dist-server/service/patch-entry/index.js.map +1 -0
  66. package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
  67. package/dist-server/service/patch-entry/patch-entry.js +96 -0
  68. package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
  69. package/dist-server/service/types.d.ts +137 -0
  70. package/dist-server/service/types.js +3 -0
  71. package/dist-server/service/types.js.map +1 -0
  72. package/dist-server/tsconfig.tsbuildinfo +1 -0
  73. package/package.json +47 -0
  74. package/server/index.ts +21 -0
  75. package/server/service/apply-patch.test.ts +640 -0
  76. package/server/service/apply-patch.ts +250 -0
  77. package/server/service/assistant.test.ts +1317 -0
  78. package/server/service/assistant.ts +1431 -0
  79. package/server/service/board-ai-resolver.ts +239 -0
  80. package/server/service/chat-message/chat-message.ts +110 -0
  81. package/server/service/chat-message/index.ts +5 -0
  82. package/server/service/chat-session/chat-session.ts +103 -0
  83. package/server/service/chat-session/index.ts +5 -0
  84. package/server/service/chat-session-resolver.ts +154 -0
  85. package/server/service/index.ts +24 -0
  86. package/server/service/patch-entry/index.ts +5 -0
  87. package/server/service/patch-entry/patch-entry.ts +89 -0
  88. package/server/service/types.ts +138 -0
  89. package/things-factory.config.js +1 -0
  90. package/translations/en.json +39 -0
  91. package/translations/ja.json +39 -0
  92. package/translations/ko.json +40 -0
  93. package/translations/ms.json +39 -0
  94. package/translations/zh.json +39 -0
  95. package/tsconfig.json +9 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-ai-chat.js","sourceRoot":"","sources":["../../client/components/board-ai-chat.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * <ox-board-ai-chat> — AI 주도 보드 모델링 채팅 컴포넌트 (Lit).\n *\n * 입력:\n * - sessionId: 영속 ChatSession 식별자 (없으면 ad-hoc 모드, 메시지 영속 안 됨)\n * - currentBoard: 현재 BoardModel JSON (호스트가 동기화)\n * - scopes / knownTypes / categories: 도메인 컨텍스트\n *\n * 출력 (이벤트):\n * - `board-edit-patch` { detail: { patch, summary, confidence, patchId } }\n * 호스트가 받아서 보드 모델에 적용 (applyBoardEditPatch helper).\n * - `chat-followup` { detail: { question } }\n *\n * 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.\n */\nimport '@material/web/icon/icon.js'\n\nimport { LitElement, css, html, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { unsafeHTML } from 'lit/directives/unsafe-html.js'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport gql from 'graphql-tag'\n\nimport { renderMarkdown } from './markdown.js'\nimport { buildChatMutationInput } from './chat-input-builder.js'\n\ninterface ChatLine {\n role: 'user' | 'assistant' | 'system'\n content: string\n patchSummary?: string\n followUp?: string\n patchId?: string\n pending?: boolean\n /** AI 가 응답을 만드는 동안 호출한 도구 trace — fold-able 박스에서 노출. */\n toolUsages?: Array<{ name: string; arguments: any; result: any; kind: 'read' | 'write' | 'unknown' }>\n /** UI 상태: tool usage 박스 펼침 여부 (assistant 메시지마다 독립). */\n toolUsagesOpen?: boolean\n}\n\nconst BOARD_AI_CHAT_MUTATION = gql`\n mutation BoardAIChat($input: BoardAIChatInput!) {\n boardAIChat(input: $input) {\n reply\n patch\n followUp\n clientId\n sessionId\n userMessageId\n assistantMessageId\n patchId\n toolUsages\n }\n }\n`\n\nconst CHAT_MESSAGES_QUERY = gql`\n query ChatMessages($sessionId: String!, $limit: Int, $offset: Int) {\n chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {\n id\n role\n content\n relatedPatchId\n toolUsagesJson\n createdAt\n }\n }\n`\n\n@customElement('ox-board-ai-chat')\nexport class OxBoardAIChat extends LitElement {\n /** 영속 세션 id. 없으면 ad-hoc (메시지 영속 안 됨). */\n @property({ type: String, attribute: 'session-id' })\n sessionId?: string\n\n /**\n * 현재 BoardModel — chat() 입력으로 그대로 전달.\n *\n * 주의: 호스트가 라이브 편집 가능한 캔버스(things-scene)를 갖고 있다면, 캔버스 안에서\n * 일어나는 수작업 편집이 이 property 에는 자동 반영되지 않는다. 이 경우 `boardProvider`\n * 콜백을 같이 넘기면 send() 시점에 라이브 보드를 pull 한다.\n */\n @property({ type: Object })\n currentBoard?: any\n\n /**\n * 라이브 보드 모델 공급자 (선택).\n *\n * 호스트가 things-scene 같은 캔버스를 사용 중이면 사용자가 캔버스에서 수작업 편집한\n * 내용은 `currentBoard` property 에 자동 반영되지 않는다. 이 콜백을 등록하면 `send()`\n * 직전에 호출해 가장 최신 보드를 pull → AI 가 항상 라이브 상태를 본다.\n *\n * 우선순위: boardProvider() ?? currentBoard.\n */\n @property({ attribute: false })\n boardProvider?: () => any\n\n /**\n * 모델러에서 현재 선택된 컴포넌트의 `refid` 목록.\n *\n * refid 는 things-scene 이 모든 컴포넌트에 자동 발급하는 universal numeric handle.\n * 호스트가 매 render 마다 `.selectedRefids=${...}` 로 갱신. AI 한테 selection 정보로\n * 그대로 전달되어 \"선택한\" / \"selected\" / \"this\" 류 지시어 해석에 사용.\n */\n @property({ type: Array, attribute: false })\n selectedRefids: number[] = []\n\n /** board-import registry scopes. */\n @property({ type: Array })\n scopes?: string[]\n\n @property({ type: Array, attribute: 'known-types' })\n knownTypes?: string[]\n\n @property({ type: Array })\n categories?: string[]\n\n /**\n * 컴포넌트 type 별 유효 속성 스킴.\n * 형태: [{ type, description?, group?, properties? }]\n * LLM 이 정확한 컴포넌트를 만들도록 mutation input 으로 forwarding.\n */\n @property({ type: Array, attribute: false })\n componentSchemas?: Array<{ type: string; description?: string; group?: string; properties?: Record<string, any> }>\n\n @property({ type: String })\n placeholder = '자연어로 명령하세요 (Shift+Enter 줄바꿈, Enter 전송)'\n\n @state()\n private lines: ChatLine[] = []\n\n @state()\n private input = ''\n\n /** 마지막 send 실패 시 보존 — 다시 시도 버튼 클릭 시 사용 */\n @state()\n private lastFailedInput?: string\n\n /** 되돌려진 patch id 들 — 한 번 revert 한 patch 는 버튼 비활성 */\n @state()\n private revertedPatchIds = new Set<string>()\n\n /** mini action — 인라인 예시 토글 */\n @state()\n private examplesOpen = false\n\n /** 복사 toast 표시 상태 */\n @state()\n private toastMessage?: string\n\n /** 복사 confirmation feedback (메시지 idx → 짧은 시간 동안 ✓ 표시) */\n @state()\n private copiedIdx?: number\n\n @state()\n private busy = false\n\n @state()\n private errorMessage?: string\n\n static styles = [\n // 어플리케이션 표준 스크롤바 (--scrollbar-width / --scrollbar-thumb-color 등 호스트 변수 따름)\n ScrollbarStyles,\n css`\n /*\n Material Design 3 토큰 사용 — 사용자 테마 (라이트/다크/브랜드) 자동 반영.\n 각 var() 의 두 번째 인자는 토큰 미정의 시 fallback (현 디자인 톤).\n */\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 240px;\n position: relative;\n font:\n 400 11px / 1.55 -apple-system,\n BlinkMacSystemFont,\n 'Inter',\n 'Pretendard',\n 'Segoe UI',\n Roboto,\n sans-serif;\n letter-spacing: -0.005em;\n color: var(--md-sys-color-on-surface, #0f172a);\n background: var(--md-sys-color-surface, #ffffff);\n }\n\n /* ── Messages ─────────────────────────────────────── */\n /* 스크롤바는 ScrollbarStyles (위쪽 styles 배열) 가 처리 — 어플리케이션 표준 따름 */\n .messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px 20px 8px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n scroll-behavior: smooth;\n }\n\n /* ── Empty state — onboarding 가이드 ───────────────── */\n .empty {\n margin: auto;\n text-align: left;\n padding: 24px 16px;\n width: 100%;\n max-width: 360px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n .empty .header {\n text-align: center;\n padding: 8px 0 4px;\n }\n .empty .icon {\n font-size: 22px;\n opacity: 0.4;\n margin-bottom: 8px;\n color: var(--md-sys-color-primary, currentColor);\n display: block;\n }\n .empty .title {\n color: var(--md-sys-color-on-surface, #0f172a);\n font-size: 12.5px;\n font-weight: 600;\n margin-bottom: 4px;\n letter-spacing: -0.01em;\n }\n .empty .subtitle {\n color: var(--md-sys-color-on-surface-variant, #475569);\n font-size: 10.5px;\n line-height: 1.5;\n }\n\n .empty .group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n .empty .group-label {\n font-size: 9.5px;\n color: var(--md-sys-color-outline, #94a3b8);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n font-weight: 600;\n padding: 0 2px;\n }\n .empty .example {\n all: unset;\n display: block;\n cursor: pointer;\n padding: 7px 10px;\n font-size: 11px;\n color: var(--md-sys-color-on-surface, #0f172a);\n background: var(--md-sys-color-surface-container, #f8fafc);\n border: 1px solid var(--md-sys-color-outline-variant, #eef2f6);\n border-radius: 8px;\n line-height: 1.5;\n transition:\n background 0.15s,\n border-color 0.15s,\n transform 0.1s;\n }\n .empty .example:hover {\n background: var(--md-sys-color-surface-container-high, #f1f5f9);\n border-color: var(--md-sys-color-outline, #cbd5e1);\n }\n .empty .example:active {\n transform: scale(0.99);\n }\n .empty .example .arrow {\n float: right;\n opacity: 0.4;\n margin-left: 8px;\n font-size: 11px;\n }\n\n .empty .footer {\n margin-top: 4px;\n padding: 8px 4px 0;\n border-top: 1px solid var(--md-sys-color-outline-variant, #eef2f6);\n color: var(--md-sys-color-outline, #94a3b8);\n font-size: 10px;\n line-height: 1.6;\n text-align: center;\n }\n .empty .footer .badge {\n display: inline-block;\n margin: 0 4px;\n padding: 1px 7px;\n background: var(--md-sys-color-surface-container, #f8fafc);\n border-radius: 8px;\n font-size: 9.5px;\n color: var(--md-sys-color-on-surface-variant, #64748b);\n }\n\n /* ── Message label (sender hint) ──────────────────── */\n .msg-label {\n font-size: 9.5px;\n color: var(--md-sys-color-outline, #94a3b8);\n letter-spacing: 0.05em;\n font-weight: 500;\n text-transform: uppercase;\n margin: 10px 0 3px 4px;\n align-self: flex-start;\n }\n .msg-label:first-child {\n margin-top: 0;\n }\n\n /* ── Message wrapper (메시지 + hover 액션) ────────── */\n .msg-wrap {\n display: flex;\n flex-direction: column;\n width: fit-content;\n max-width: min(78%, 480px);\n margin: 2px 0;\n }\n .msg-wrap.user {\n align-self: flex-end;\n align-items: flex-end;\n }\n .msg-wrap.assistant {\n align-self: flex-start;\n align-items: flex-start;\n }\n .msg-wrap.system {\n align-self: center;\n align-items: center;\n max-width: 100%;\n }\n\n /* ── Message bubble ───────────────────────────────── */\n .msg {\n display: inline-block;\n width: fit-content;\n max-width: 100%;\n padding: 6px 10px;\n border-radius: 12px;\n white-space: normal;\n overflow-wrap: anywhere;\n font-size: 11px;\n line-height: 1.55;\n }\n\n .msg.user {\n align-self: flex-end;\n background: var(--md-sys-color-primary, #1e293b);\n color: var(--md-sys-color-on-primary, #f8fafc);\n border-radius: 12px 12px 3px 12px;\n margin-top: 6px;\n }\n\n .msg.assistant {\n align-self: flex-start;\n background: var(--md-sys-color-surface-container, #f8fafc);\n color: var(--md-sys-color-on-surface, #0f172a);\n border: 1px solid var(--md-sys-color-outline-variant, #eef2f6);\n border-radius: 12px 12px 12px 3px;\n }\n\n /* ── Markdown 렌더 (AI 응답) ──────────────────────── */\n .md-body {\n display: block;\n }\n .md-body > :first-child {\n margin-top: 0;\n }\n .md-body > :last-child {\n margin-bottom: 0;\n }\n .md-body p {\n margin: 0 0 6px;\n line-height: 1.55;\n }\n .md-body ul,\n .md-body ol {\n margin: 4px 0 6px;\n padding-left: 18px;\n }\n .md-body li {\n margin: 1px 0;\n line-height: 1.5;\n }\n .md-body li > p {\n margin: 0;\n }\n .md-body strong {\n font-weight: 600;\n }\n .md-body em {\n font-style: italic;\n }\n .md-body code {\n font-family:\n 'SF Mono',\n Menlo,\n Monaco,\n Consolas,\n monospace;\n font-size: 10px;\n padding: 1px 4px;\n background: var(--md-sys-color-surface-container-high, rgba(15, 23, 42, 0.06));\n border-radius: 3px;\n }\n .md-body pre {\n margin: 5px 0;\n padding: 6px 8px;\n background: var(--md-sys-color-surface-container-high, rgba(15, 23, 42, 0.06));\n border-radius: 5px;\n overflow-x: auto;\n font-size: 10px;\n line-height: 1.5;\n }\n /* user 메시지(어두운 배경) 위에서는 code/pre 의 contrast 보정 */\n .msg.user .md-body code,\n .msg.user .md-body pre {\n background: rgba(255, 255, 255, 0.15);\n }\n .msg.user .md-body a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .msg.user .md-body blockquote {\n border-left-color: rgba(255, 255, 255, 0.3);\n color: rgba(255, 255, 255, 0.85);\n }\n .msg.user .md-body hr {\n border-top-color: rgba(255, 255, 255, 0.2);\n }\n .md-body pre code {\n padding: 0;\n background: transparent;\n font-size: inherit;\n }\n .md-body a {\n color: var(--md-sys-color-primary, #1e293b);\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .md-body a:hover {\n text-decoration: none;\n }\n .md-body h1,\n .md-body h2,\n .md-body h3,\n .md-body h4 {\n margin: 6px 0 3px;\n font-weight: 600;\n line-height: 1.4;\n }\n .md-body h1 { font-size: 13px; }\n .md-body h2 { font-size: 12px; }\n .md-body h3,\n .md-body h4 { font-size: 11.5px; }\n .md-body blockquote {\n margin: 4px 0;\n padding: 1px 8px;\n border-left: 2px solid var(--md-sys-color-outline-variant, #cbd5e1);\n color: var(--md-sys-color-on-surface-variant, #64748b);\n }\n .md-body hr {\n border: 0;\n border-top: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);\n margin: 6px 0;\n }\n .md-body table {\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 10px;\n }\n .md-body th,\n .md-body td {\n border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);\n padding: 2px 6px;\n }\n .md-body th {\n background: var(--md-sys-color-surface-container, #f8fafc);\n font-weight: 600;\n }\n\n .msg.system {\n align-self: center;\n background: transparent;\n color: var(--md-sys-color-outline, #94a3b8);\n font-size: 10px;\n font-style: italic;\n max-width: 100%;\n padding: 3px 8px;\n margin: 3px 0;\n border: 0;\n }\n\n .msg.pending {\n opacity: 0.5;\n }\n .msg.pending::after {\n content: '';\n display: inline-block;\n width: 6px;\n height: 6px;\n margin-left: 6px;\n border-radius: 50%;\n background: currentColor;\n opacity: 0.4;\n animation: pulse 1.2s ease-in-out infinite;\n }\n @keyframes pulse {\n 0%, 100% { opacity: 0.2; transform: scale(0.85); }\n 50% { opacity: 0.7; transform: scale(1.1); }\n }\n\n /* ── Patch indicator + 인라인 되돌리기 ─────────────── */\n .summary {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n padding: 3px 4px 3px 8px;\n background: var(--md-sys-color-surface-container-high, #f1f5f9);\n border-radius: 6px;\n font-size: 10px;\n color: var(--md-sys-color-on-surface-variant, #475569);\n line-height: 1;\n }\n .summary md-icon {\n --md-icon-size: 13px;\n color: var(--md-sys-color-tertiary, #10b981);\n }\n .summary.reverted {\n opacity: 0.6;\n }\n .summary.reverted md-icon {\n color: var(--md-sys-color-outline, #94a3b8);\n }\n .summary .summary-text {\n letter-spacing: 0.01em;\n }\n .summary .revert-btn {\n height: 20px;\n padding: 0 8px;\n background: transparent;\n border: 0;\n border-radius: 4px;\n cursor: pointer;\n font-family: inherit;\n letter-spacing: inherit;\n font-size: 10px;\n color: var(--md-sys-color-on-surface-variant, #475569);\n line-height: 1;\n transition: background 0.12s, color 0.12s;\n width: auto;\n }\n .summary .revert-btn:hover:not(:disabled) {\n background: var(--md-sys-color-surface, #ffffff);\n color: var(--md-sys-color-on-surface, #0f172a);\n }\n .summary.reverted .revert-btn,\n .summary .revert-btn:disabled {\n display: none;\n }\n\n /* ── Follow-up question ───────────────────────────── */\n .followup {\n font-size: 10.5px;\n color: var(--md-sys-color-on-secondary-container, #92400e);\n background: var(--md-sys-color-secondary-container, #fffbeb);\n border-left: 2px solid var(--md-sys-color-secondary, #f59e0b);\n padding: 5px 8px;\n border-radius: 0 5px 5px 0;\n margin-top: 6px;\n line-height: 1.5;\n }\n\n /* ── Tool usages fold-able 박스 — AI 도구 호출 trace ───── */\n .tool-usages {\n margin-top: 6px;\n border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);\n border-radius: 6px;\n background: var(--md-sys-color-surface-container-low, #f8fafc);\n overflow: hidden;\n font-size: 10.5px;\n }\n .tool-usages-header {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 5px 8px;\n background: transparent;\n border: 0;\n cursor: pointer;\n color: var(--md-sys-color-on-surface-variant, #475569);\n font-family: inherit;\n font-size: inherit;\n letter-spacing: inherit;\n text-align: left;\n }\n .tool-usages-header:hover {\n background: var(--md-sys-color-surface-container, #f1f5f9);\n }\n .tool-usages-icon,\n .tool-usages-chevron {\n --md-icon-size: 14px;\n flex-shrink: 0;\n }\n .tool-usages-summary {\n flex: 1;\n font-weight: 500;\n }\n .tool-usages-counts {\n color: var(--md-sys-color-outline, #94a3b8);\n font-weight: 400;\n font-size: 10px;\n }\n .tool-usages-list {\n list-style: none;\n margin: 0;\n padding: 0 8px 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .tool-usage-item {\n border-left: 2px solid var(--md-sys-color-outline-variant, #cbd5e1);\n padding: 4px 8px 4px 8px;\n background: var(--md-sys-color-surface, #ffffff);\n border-radius: 0 4px 4px 0;\n }\n .tool-usage-item.kind-read {\n border-left-color: var(--md-sys-color-tertiary, #10b981);\n }\n .tool-usage-item.kind-write {\n border-left-color: var(--md-sys-color-primary, #1e293b);\n }\n .tool-usage-item.kind-unknown {\n border-left-color: var(--md-sys-color-error, #b91c1c);\n }\n .tool-usage-head {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 3px;\n }\n .tool-usage-step {\n color: var(--md-sys-color-outline, #94a3b8);\n font-size: 10px;\n }\n .tool-usage-name {\n font-weight: 600;\n color: var(--md-sys-color-on-surface, #0f172a);\n font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;\n font-size: 10.5px;\n }\n .tool-usage-kind {\n font-size: 9.5px;\n padding: 1px 5px;\n border-radius: 3px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n font-weight: 600;\n }\n .tool-usage-kind.kind-read {\n color: var(--md-sys-color-on-tertiary, #ffffff);\n background: var(--md-sys-color-tertiary, #10b981);\n }\n .tool-usage-kind.kind-write {\n color: var(--md-sys-color-on-primary, #ffffff);\n background: var(--md-sys-color-primary, #1e293b);\n }\n .tool-usage-kind.kind-unknown {\n color: var(--md-sys-color-on-error, #ffffff);\n background: var(--md-sys-color-error, #b91c1c);\n }\n .tool-usage-args,\n .tool-usage-result {\n margin: 2px 0 0;\n padding: 4px 6px;\n background: var(--md-sys-color-surface-container-high, rgba(15, 23, 42, 0.04));\n border-radius: 3px;\n font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;\n font-size: 10px;\n line-height: 1.4;\n white-space: pre-wrap;\n overflow-x: auto;\n color: var(--md-sys-color-on-surface, #0f172a);\n max-height: 180px;\n overflow-y: auto;\n }\n .tool-usage-args {\n border-left: 2px solid var(--md-sys-color-outline-variant, #cbd5e1);\n }\n .tool-usage-result {\n border-left: 2px solid var(--md-sys-color-outline, #94a3b8);\n }\n\n /* ── Hover actions (assistant 메시지만, Material icon button) ── */\n .msg-actions {\n display: flex;\n gap: 2px;\n margin-top: 2px;\n padding: 0;\n opacity: 0;\n transition: opacity 0.15s;\n pointer-events: none;\n }\n .msg-wrap:hover .msg-actions,\n .msg-wrap:focus-within .msg-actions {\n opacity: 1;\n pointer-events: auto;\n }\n .msg-actions .msg-action {\n width: 22px;\n height: 22px;\n padding: 0;\n background: transparent;\n border: 0;\n border-radius: 5px;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--md-sys-color-outline, #94a3b8);\n transition:\n background 0.12s,\n color 0.12s;\n }\n .msg-actions .msg-action:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high, #e2e8f0);\n color: var(--md-sys-color-on-surface, #0f172a);\n }\n .msg-actions .msg-action:disabled {\n opacity: 0.4;\n cursor: default;\n }\n .msg-actions .msg-action.confirmed {\n color: var(--md-sys-color-tertiary, #10b981);\n }\n .msg-actions .msg-action md-icon {\n --md-icon-size: 14px;\n }\n\n /* ── Error banner ─────────────────────────────────── */\n .error {\n color: var(--md-sys-color-on-error-container, #b91c1c);\n padding: 7px 11px;\n background: var(--md-sys-color-error-container, #fef2f2);\n border: 1px solid var(--md-sys-color-error, #fee2e2);\n border-radius: 8px;\n margin: 0 16px 8px;\n font-size: 10.5px;\n line-height: 1.5;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n }\n .error .error-text {\n flex: 1;\n }\n .error .retry-btn {\n flex-shrink: 0;\n padding: 3px 8px;\n font-size: 10px;\n font-family: inherit;\n letter-spacing: inherit;\n background: var(--md-sys-color-error, #b91c1c);\n color: var(--md-sys-color-on-error, #ffffff);\n border: 0;\n border-radius: 6px;\n cursor: pointer;\n transition: opacity 0.15s;\n width: auto;\n height: auto;\n }\n .error .retry-btn:hover {\n opacity: 0.88;\n }\n\n /* ── Composer (actions + input 통합 영역) ──────────── */\n .composer {\n border-top: 1px solid var(--md-sys-color-outline-variant, #f1f5f9);\n background: var(--md-sys-color-surface, #ffffff);\n display: flex;\n flex-direction: column;\n }\n\n /* ── Mini action row (메시지 시작 후) ──────────────── */\n .actions-row {\n display: flex;\n gap: 6px;\n padding: 10px 16px 6px;\n flex-wrap: wrap;\n align-items: center;\n }\n /* Material 3 assist-chip 톤 — height 24, font label-small */\n .action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n height: 24px;\n padding: 0 10px;\n font-size: 11px;\n font-family: inherit;\n letter-spacing: inherit;\n line-height: 1;\n color: var(--md-sys-color-on-surface-variant, #475569);\n background: transparent;\n border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);\n border-radius: 8px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n width: auto;\n vertical-align: middle;\n }\n .action:hover {\n background: var(--md-sys-color-surface-container, #f8fafc);\n border-color: var(--md-sys-color-outline, #cbd5e1);\n color: var(--md-sys-color-on-surface, #0f172a);\n }\n .action:active {\n transform: scale(0.97);\n }\n .action md-icon {\n --md-icon-size: 14px;\n opacity: 0.7;\n }\n\n /* ── Inline examples (action row 의 \"예시\" 토글 시) ──── */\n .inline-examples {\n padding: 4px 16px 0;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n .inline-examples .ex-row {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n }\n .inline-examples .ex-row .ex-label {\n font-size: 9px;\n color: var(--md-sys-color-outline, #94a3b8);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n margin-right: 2px;\n align-self: center;\n font-weight: 600;\n }\n .inline-examples .ex {\n all: unset;\n cursor: pointer;\n padding: 2px 8px;\n font-size: 10px;\n color: var(--md-sys-color-on-surface, #0f172a);\n background: var(--md-sys-color-surface-container, #f8fafc);\n border: 1px solid var(--md-sys-color-outline-variant, #eef2f6);\n border-radius: 10px;\n transition: background 0.15s;\n }\n .inline-examples .ex:hover {\n background: var(--md-sys-color-surface-container-high, #f1f5f9);\n }\n\n /* ── Input area ───────────────────────────────────── */\n /* border-top 은 .composer 가 담당 (actions-row 가 같은 wrapper 안) */\n .input-row {\n display: flex;\n gap: 10px;\n padding: 6px 16px 14px;\n background: transparent;\n align-items: flex-end;\n }\n /* 메시지 시작 전 — actions-row 가 없을 때는 input-row 가 직접 border-top */\n .composer.no-actions .input-row {\n padding-top: 14px;\n }\n\n textarea {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);\n border-radius: 12px;\n font: inherit;\n letter-spacing: inherit;\n resize: none;\n min-height: 36px;\n max-height: 140px;\n outline: none;\n transition:\n border-color 0.2s,\n box-shadow 0.2s;\n background: var(--md-sys-color-surface, #ffffff);\n color: var(--md-sys-color-on-surface, #0f172a);\n }\n textarea::placeholder {\n color: var(--md-sys-color-outline, #94a3b8);\n }\n textarea:focus {\n border-color: var(--md-sys-color-outline, #cbd5e1);\n box-shadow: 0 0 0 3px var(--md-sys-color-primary-container, rgba(15, 23, 42, 0.08));\n }\n textarea:disabled {\n background: var(--md-sys-color-surface-container, #f8fafc);\n color: var(--md-sys-color-outline, #94a3b8);\n cursor: not-allowed;\n }\n\n /* ── Send button — ChatGPT 스타일 원형 ↑ (.input-row 안의 button 만) ─── */\n .input-row > button {\n width: 32px;\n height: 32px;\n padding: 0;\n background: var(--md-sys-color-primary, #1e293b);\n color: var(--md-sys-color-on-primary, #ffffff);\n border: 0;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.15s,\n transform 0.1s,\n opacity 0.15s;\n flex-shrink: 0;\n align-self: flex-end;\n margin-bottom: 2px;\n }\n .input-row > button:disabled {\n background: var(--md-sys-color-surface-container-highest, #e2e8f0);\n color: var(--md-sys-color-outline, #94a3b8);\n cursor: not-allowed;\n }\n .input-row > button:hover:not(:disabled) {\n opacity: 0.88;\n }\n .input-row > button:active:not(:disabled) {\n transform: scale(0.94);\n }\n .input-row > button md-icon {\n --md-icon-size: 18px;\n }\n .input-row > button .spinner {\n width: 12px;\n height: 12px;\n border: 1.8px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spin 0.7s linear infinite;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n `\n ]\n\n connectedCallback() {\n super.connectedCallback()\n if (this.sessionId) {\n this.loadHistory()\n }\n }\n\n updated(changed: Map<string, any>) {\n if (changed.has('sessionId') && this.sessionId && !changed.get('sessionId')) {\n this.loadHistory()\n }\n // 자동 스크롤\n const ms = this.renderRoot.querySelector('.messages') as HTMLElement | null\n if (ms) ms.scrollTop = ms.scrollHeight\n }\n\n private async loadHistory() {\n if (!this.sessionId) return\n try {\n const result = await client.query({\n query: CHAT_MESSAGES_QUERY,\n variables: { sessionId: this.sessionId, limit: 200, offset: 0 },\n fetchPolicy: 'network-only'\n })\n const msgs = result.data?.chatMessages ?? []\n this.lines = msgs.map((m: any) => ({\n role: m.role,\n content: m.content,\n patchId: m.relatedPatchId ?? undefined,\n toolUsages: Array.isArray(m.toolUsagesJson) ? m.toolUsagesJson : undefined\n }))\n } catch (e: any) {\n this.errorMessage = `${i18next.t('board-ai.text.failed-to-load-history')}: ${e.message ?? e}`\n }\n }\n\n render() {\n const hasMessages = this.lines.length > 0\n return html`\n ${this.toastMessage\n ? html`<div class=\"toast show\">${this.toastMessage}</div>`\n : nothing}\n <div class=\"messages\">\n ${!hasMessages ? this.renderEmpty() : nothing}\n ${this.lines.map((line, idx) => {\n const prev = this.lines[idx - 1]\n const showLabel = line.role === 'assistant' && (!prev || prev.role !== 'assistant')\n const content = (line.content ?? '').trim()\n const isMarkdown = line.role !== 'system' && content.length > 0\n const showActions = line.role !== 'system' && !line.pending && content.length > 0\n return html`\n ${showLabel\n ? html`<div class=\"msg-label\">${i18next.t('board-ai.label.ai')}</div>`\n : nothing}\n <div class=\"msg-wrap ${line.role}\">\n <div class=\"msg ${line.role} ${line.pending ? 'pending' : ''}\">\n ${isMarkdown\n ? html`<div class=\"md-body\">${unsafeHTML(renderMarkdown(content))}</div>`\n : content}\n ${line.patchId\n ? html`\n <div class=\"summary ${this.revertedPatchIds.has(line.patchId) ? 'reverted' : ''}\">\n <md-icon>check_circle</md-icon>\n <span class=\"summary-text\">\n ${this.revertedPatchIds.has(line.patchId)\n ? i18next.t('board-ai.text.reverted')\n : i18next.t('board-ai.text.applied-changes')}\n </span>\n <button\n class=\"revert-btn\"\n ?disabled=${this.revertedPatchIds.has(line.patchId)}\n @click=${() => line.patchId && this.onRevertClick(line.patchId)}>\n ${i18next.t('board-ai.button.revert')}\n </button>\n </div>\n `\n : nothing}\n ${line.followUp\n ? html`<div class=\"followup\">${unsafeHTML(renderMarkdown(line.followUp.trim()))}</div>`\n : nothing}\n ${line.toolUsages && line.toolUsages.length > 0\n ? this.renderToolUsages(idx, line)\n : nothing}\n </div>\n ${showActions ? this.renderMessageActions(idx, line, content) : nothing}\n </div>\n `\n })}\n </div>\n ${this.errorMessage\n ? html`\n <div class=\"error\">\n <span class=\"error-text\">${this.errorMessage}</span>\n ${this.lastFailedInput\n ? html`\n <button class=\"retry-btn\" @click=${this.retryLastSend}>\n ${i18next.t('board-ai.button.retry')}\n </button>\n `\n : nothing}\n </div>\n `\n : nothing}\n <div class=\"composer ${hasMessages ? '' : 'no-actions'}\">\n ${hasMessages\n ? html`\n <div class=\"actions-row\">\n <button\n class=\"action\"\n @click=${this.clearChat}\n title=${i18next.t('board-ai.text.clear-chat-tooltip')}>\n <md-icon>refresh</md-icon>\n ${i18next.t('board-ai.button.new-conversation')}\n </button>\n <button\n class=\"action\"\n @click=${() => (this.examplesOpen = !this.examplesOpen)}\n title=${i18next.t('board-ai.text.show-examples-tooltip')}>\n <md-icon>${this.examplesOpen ? 'close' : 'lightbulb'}</md-icon>\n ${this.examplesOpen\n ? i18next.t('board-ai.button.close')\n : i18next.t('board-ai.button.examples')}\n </button>\n </div>\n ${this.examplesOpen ? this.renderInlineExamples() : nothing}\n `\n : nothing}\n <div class=\"input-row\">\n <textarea\n .value=${this.input}\n @input=${(e: any) => (this.input = e.target.value)}\n @keydown=${this.onKeyDown}\n ?disabled=${this.busy}\n rows=\"1\"\n placeholder=${this.placeholder ?? i18next.t('board-ai.text.input-placeholder')}></textarea>\n <button\n ?disabled=${this.busy || !this.input.trim()}\n @click=${this.send}\n title=${i18next.t('board-ai.text.send-tooltip')}>\n ${this.busy\n ? html`<span class=\"spinner\"></span>`\n : html`<md-icon>arrow_upward</md-icon>`}\n </button>\n </div>\n </div>\n `\n }\n\n private onKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' && !e.shiftKey && !this.busy) {\n e.preventDefault()\n this.send()\n }\n }\n\n // ── Empty state with onboarding guide ──────────────────────────\n\n /** 예시 그룹 — i18n 키. localize mixin 이 언어 변경 시 자동 재렌더. */\n private static readonly EXAMPLE_GROUPS: Array<{\n labelKey: string\n itemKeys: string[]\n }> = [\n {\n labelKey: 'board-ai.label.create',\n itemKeys: [\n 'board-ai.example.create-monitoring-dashboard',\n 'board-ai.example.create-welcome-screen'\n ]\n },\n {\n labelKey: 'board-ai.label.edit',\n itemKeys: [\n 'board-ai.example.edit-align-distribute',\n 'board-ai.example.edit-resize-board'\n ]\n },\n {\n labelKey: 'board-ai.label.style',\n itemKeys: [\n 'board-ai.example.style-dark-mode',\n 'board-ai.example.style-rounded-shadow'\n ]\n }\n ]\n\n private renderEmpty() {\n return html`\n <div class=\"empty\">\n <div class=\"header\">\n <md-icon class=\"icon\">auto_awesome</md-icon>\n <div class=\"title\">${i18next.t('board-ai.text.empty-title')}</div>\n <div class=\"subtitle\">${i18next.t('board-ai.text.empty-subtitle')}</div>\n </div>\n\n ${OxBoardAIChat.EXAMPLE_GROUPS.map(\n group => html`\n <div class=\"group\">\n <div class=\"group-label\">${i18next.t(group.labelKey)}</div>\n ${group.itemKeys.map(key => {\n const text = i18next.t(key)\n return html`\n <button class=\"example\" @click=${() => this.useExample(text)}>\n ${text}<span class=\"arrow\">→</span>\n </button>\n `\n })}\n </div>\n `\n )}\n\n <div class=\"footer\">\n <span class=\"badge\">${i18next.t('board-ai.label.korean-supported')}</span>\n <span class=\"badge\">${i18next.t('board-ai.label.multi-command')}</span>\n <span class=\"badge\">${i18next.t('board-ai.label.review-able')}</span>\n <br />\n ${i18next.t('board-ai.text.empty-footer-notice')}\n </div>\n </div>\n `\n }\n\n /** 예시 클릭 — 입력창에 채우고 포커스 (사용자가 수정 후 전송 가능) */\n private useExample(text: string) {\n this.input = text\n this.examplesOpen = false\n requestAnimationFrame(() => {\n const ta = this.renderRoot.querySelector('textarea') as HTMLTextAreaElement | null\n ta?.focus()\n ta?.setSelectionRange(text.length, text.length)\n })\n }\n\n // ── Mini actions ───────────────────────────────────────────────\n\n /** \"새 대화\" — UI 의 메시지 라인 비움 (서버 ChatSession 이력은 그대로 보존) */\n private clearChat() {\n this.lines = []\n this.input = ''\n this.errorMessage = undefined\n this.lastFailedInput = undefined\n this.examplesOpen = false\n this.revertedPatchIds = new Set()\n }\n\n private renderInlineExamples() {\n return html`\n <div class=\"inline-examples\">\n ${OxBoardAIChat.EXAMPLE_GROUPS.map(\n group => html`\n <div class=\"ex-row\">\n <span class=\"ex-label\">${i18next.t(group.labelKey)}</span>\n ${group.itemKeys.map(key => {\n const t = i18next.t(key)\n return html`<button class=\"ex\" @click=${() => this.useExample(t)}>${t}</button>`\n })}\n </div>\n `\n )}\n </div>\n `\n }\n\n // ── Retry (에러 발생 시) ───────────────────────────────────────\n\n private async retryLastSend() {\n if (!this.lastFailedInput) return\n this.input = this.lastFailedInput\n this.errorMessage = undefined\n this.lastFailedInput = undefined\n await this.send()\n }\n\n // ── Per-message hover action row ───────────────────────────────\n\n /**\n * 메시지별 hover 액션 — 텍스트 chip 으로 명확.\n * user : 액션 row 자체 X (자기 메시지에 시각 노이즈 X — 편집은 input 위 \"새 대화\" 흐름 또는 마우스 더블클릭)\n * assistant : 복사 + 재생성 + (patch 적용된 경우만) 되돌리기\n * system : 없음\n */\n private renderMessageActions(idx: number, line: ChatLine, content: string) {\n if (line.role !== 'assistant') return nothing\n\n const isCopied = this.copiedIdx === idx\n // revert 는 인라인 patch chip (메시지 안) 한 곳에만 — hover 액션에서 제외 (중복 방지).\n return html`\n <div class=\"msg-actions\">\n <button\n class=\"msg-action ${isCopied ? 'confirmed' : ''}\"\n title=${isCopied\n ? i18next.t('board-ai.text.copied')\n : i18next.t('board-ai.button.copy')}\n @click=${() => this.copyMessage(idx, content)}>\n <md-icon>${isCopied ? 'check' : 'content_copy'}</md-icon>\n </button>\n <button\n class=\"msg-action\"\n ?disabled=${this.busy}\n title=${i18next.t('board-ai.button.regenerate')}\n @click=${() => this.regenerateAssistant(idx)}>\n <md-icon>refresh</md-icon>\n </button>\n </div>\n `\n }\n\n /**\n * \"AI 가 이런 도구를 사용했습니다\" fold-able 박스.\n *\n * - 기본 닫힘 (toolUsagesOpen=false). 헤더 클릭 시 toggle.\n * - 펼쳤을 때 도구 별로 name + arguments + result + read/write 색상 배지.\n * - 디버그 / 신뢰도 향상용 — 사용자가 \"왜 이렇게 답했지\" 의문 가질 때 즉시 검증.\n */\n private renderToolUsages(idx: number, line: ChatLine) {\n const usages = line.toolUsages\n if (!usages || usages.length === 0) return nothing\n const open = !!line.toolUsagesOpen\n const readCount = usages.filter(u => u.kind === 'read').length\n const writeCount = usages.filter(u => u.kind === 'write').length\n const toolsLabel = i18next.t('board-ai.text.tools-used', { defaultValue: '도구 사용' })\n return html`\n <div class=\"tool-usages ${open ? 'open' : ''}\">\n <button\n class=\"tool-usages-header\"\n aria-expanded=${open}\n @click=${() => this.toggleToolUsages(idx)}>\n <md-icon class=\"tool-usages-icon\">${open ? 'expand_less' : 'build'}</md-icon>\n <span class=\"tool-usages-summary\">\n <strong>${usages.length}</strong> ${toolsLabel}\n <span class=\"tool-usages-counts\">· read ${readCount} · write ${writeCount}</span>\n </span>\n <md-icon class=\"tool-usages-chevron\">${open ? 'expand_less' : 'expand_more'}</md-icon>\n </button>\n ${open\n ? html`\n <ol class=\"tool-usages-list\">\n ${usages.map(\n (u, i) => html`\n <li class=\"tool-usage-item kind-${u.kind}\">\n <div class=\"tool-usage-head\">\n <span class=\"tool-usage-step\">${i + 1}.</span>\n <span class=\"tool-usage-name\">${u.name}</span>\n <span class=\"tool-usage-kind kind-${u.kind}\">${u.kind}</span>\n </div>\n ${u.arguments && Object.keys(u.arguments).length > 0\n ? html`<pre class=\"tool-usage-args\">${this.formatJson(u.arguments)}</pre>`\n : nothing}\n <pre class=\"tool-usage-result\">${this.formatJson(u.result)}</pre>\n </li>\n `\n )}\n </ol>\n `\n : nothing}\n </div>\n `\n }\n\n private toggleToolUsages(idx: number) {\n const next = [...this.lines]\n const line = next[idx]\n if (!line) return\n next[idx] = { ...line, toolUsagesOpen: !line.toolUsagesOpen }\n this.lines = next\n }\n\n private formatJson(value: any): string {\n try {\n return JSON.stringify(value, null, 2)\n } catch {\n return String(value)\n }\n }\n\n // ── Per-message actions ────────────────────────────────────────\n\n private async copyMessage(idx: number, content: string) {\n try {\n await navigator.clipboard.writeText(content)\n this.copiedIdx = idx\n this.showToast(i18next.t('board-ai.text.copied'))\n setTimeout(() => {\n if (this.copiedIdx === idx) this.copiedIdx = undefined\n }, 1500)\n } catch {\n // clipboard 권한 없거나 비-secure context — 조용히 무시\n }\n }\n\n private showToast(message: string) {\n this.toastMessage = message\n setTimeout(() => {\n if (this.toastMessage === message) this.toastMessage = undefined\n }, 1500)\n }\n\n /**\n * 사용자 메시지 편집 — 해당 메시지 + 이후 모두 UI 에서 제거하고 입력창에 채움.\n * 서버 영속 history 는 그대로 유지 (다음 send 시 새 메시지로 추가).\n */\n private editUserMessage(idx: number, content: string) {\n this.lines = this.lines.slice(0, idx)\n this.input = content\n requestAnimationFrame(() => {\n const ta = this.renderRoot.querySelector('textarea') as HTMLTextAreaElement | null\n ta?.focus()\n ta?.setSelectionRange(content.length, content.length)\n })\n }\n\n /**\n * AI 응답 재생성 — 해당 assistant 메시지 + 이후 제거하고, 직전 user 메시지를 다시 send.\n */\n private async regenerateAssistant(idx: number) {\n if (this.busy) return\n // 직전 user 메시지 찾기\n let userIdx = idx - 1\n while (userIdx >= 0 && this.lines[userIdx].role !== 'user') userIdx--\n if (userIdx < 0) return\n const userContent = this.lines[userIdx].content\n // assistant 메시지 + 이후 제거\n this.lines = this.lines.slice(0, idx)\n this.input = userContent\n await this.send()\n }\n\n // ── Patch revert ───────────────────────────────────────────────\n\n private onRevertClick(patchId: string) {\n if (this.revertedPatchIds.has(patchId)) return\n this.revertedPatchIds = new Set([...this.revertedPatchIds, patchId])\n // 호스트 (board-modeller-page) 에게 revert 요청 — 호스트가 snapshot 복원 + 서버 mutation 호출\n this.dispatchEvent(\n new CustomEvent('board-edit-revert', {\n detail: { patchId },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private async send() {\n const text = this.input.trim()\n if (!text || this.busy) return\n\n this.lines = [...this.lines, { role: 'user', content: text }]\n this.input = ''\n this.busy = true\n this.errorMessage = undefined\n\n // 가짜 pending 응답 표시 (UX 부드럽게)\n const pendingIdx = this.lines.length\n this.lines = [...this.lines, { role: 'assistant', content: '', pending: true }]\n\n try {\n // LLM 으로 보낼 history — user/assistant 만 (system 은 백엔드가 자동 합류)\n const history = this.lines\n .slice(0, pendingIdx) // pending 제외\n .filter(l => l.role !== 'system')\n .map(l => ({ role: l.role, content: l.content }))\n\n // 라이브 보드 우선 — 호스트의 캔버스가 사용자 수작업 편집을 들고 있을 수 있음.\n // boardProvider 가 있으면 send 시점에 그것을 pull, 없으면 정적 currentBoard.\n const liveBoard = this.boardProvider ? this.boardProvider() : this.currentBoard\n\n const result = await client.mutate({\n mutation: BOARD_AI_CHAT_MUTATION,\n variables: {\n input: buildChatMutationInput({\n sessionId: this.sessionId,\n history,\n liveBoard,\n scopes: this.scopes,\n knownTypes: this.knownTypes,\n categories: this.categories,\n componentSchemas: this.componentSchemas,\n selectedRefids: this.selectedRefids ?? []\n })\n }\n })\n\n const out = result.data?.boardAIChat\n if (!out) throw new Error(i18next.t('board-ai.text.empty-response'))\n\n // pending 자리에 실제 응답\n const replaced = [...this.lines]\n replaced[pendingIdx] = {\n role: 'assistant',\n content: out.reply,\n patchSummary: out.patch?.summary,\n followUp: out.followUp || undefined,\n patchId: out.patchId || undefined,\n toolUsages: Array.isArray(out.toolUsages) ? out.toolUsages : undefined\n }\n this.lines = replaced\n\n // 호스트로 patch 이벤트 전파\n if (out.patch) {\n this.dispatchEvent(\n new CustomEvent('board-edit-patch', {\n detail: {\n patch: out.patch,\n summary: out.patch.summary,\n confidence: out.patch.confidence,\n patchId: out.patchId,\n sessionId: out.sessionId\n },\n bubbles: true,\n composed: true\n })\n )\n }\n if (out.followUp) {\n this.dispatchEvent(\n new CustomEvent('chat-followup', {\n detail: { question: out.followUp },\n bubbles: true,\n composed: true\n })\n )\n }\n } catch (e: any) {\n // pending 메시지 제거 + 에러 표시 + 마지막 입력 보존 (다시 시도용)\n this.lines = this.lines.slice(0, pendingIdx)\n this.errorMessage = e?.message ?? String(e)\n this.lastFailedInput = text\n } finally {\n this.busy = false\n }\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-board-ai-chat': OxBoardAIChat\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * board-ai-chat.ts 의 pure helper 회귀 방지.
3
+ *
4
+ * 핵심 보호:
5
+ * - GraphQL mutation input 에 selectedRefids 가 항상 포함되는지 — 누락이 사용자
6
+ * 보고된 "AI 가 선택된 컴포넌트를 모른다" 의 root cause 였다.
7
+ * - 식별자 정책 — refid (universal numeric) 가 selection 채널의 표준. id 는 데이터
8
+ * 바인딩 이름이며 unique 가 아니라 selection 에 부적합.
9
+ */
10
+ import { buildChatMutationInput } from './chat-input-builder';
11
+ describe('buildChatMutationInput — mutation input 회귀 방지', () => {
12
+ test('selectedRefids 가 input 에 반드시 포함된다 (root cause regression)', () => {
13
+ const input = buildChatMutationInput({
14
+ sessionId: 'session-1',
15
+ history: [{ role: 'user', content: 'hi' }],
16
+ liveBoard: { width: 1000, height: 600, components: [] },
17
+ selectedRefids: [35]
18
+ });
19
+ expect(input).toHaveProperty('selectedRefids', [35]);
20
+ });
21
+ test('selectedRefids 빈 배열도 누락 없이 포함', () => {
22
+ const input = buildChatMutationInput({
23
+ history: [],
24
+ liveBoard: null,
25
+ selectedRefids: []
26
+ });
27
+ expect(input.selectedRefids).toEqual([]);
28
+ });
29
+ test('모든 expected 필드 존재', () => {
30
+ const input = buildChatMutationInput({
31
+ sessionId: 's1',
32
+ history: [{ role: 'user', content: 'hi' }],
33
+ liveBoard: { components: [] },
34
+ scopes: ['fmsim'],
35
+ knownTypes: ['rect'],
36
+ categories: ['equipment'],
37
+ componentSchemas: [{ type: 'rect' }],
38
+ selectedRefids: [1, 2]
39
+ });
40
+ expect(Object.keys(input).sort()).toEqual([
41
+ 'categories',
42
+ 'componentSchemas',
43
+ 'currentBoard',
44
+ 'knownTypes',
45
+ 'messages',
46
+ 'scopes',
47
+ 'selectedRefids',
48
+ 'sessionId'
49
+ ]);
50
+ });
51
+ test('sessionId 누락 → null 정규화', () => {
52
+ const input = buildChatMutationInput({
53
+ history: [],
54
+ liveBoard: null,
55
+ selectedRefids: []
56
+ });
57
+ expect(input.sessionId).toBeNull();
58
+ });
59
+ test('liveBoard 누락 → null 정규화', () => {
60
+ const input = buildChatMutationInput({
61
+ history: [],
62
+ liveBoard: undefined,
63
+ selectedRefids: []
64
+ });
65
+ expect(input.currentBoard).toBeNull();
66
+ });
67
+ test('messages 가 history 그대로', () => {
68
+ const history = [
69
+ { role: 'user', content: 'msg1' },
70
+ { role: 'assistant', content: 'reply1' }
71
+ ];
72
+ const input = buildChatMutationInput({
73
+ history,
74
+ liveBoard: null,
75
+ selectedRefids: []
76
+ });
77
+ expect(input.messages).toBe(history);
78
+ });
79
+ test('liveBoard 가 그대로 currentBoard 로 매핑', () => {
80
+ const board = {
81
+ width: 1920,
82
+ height: 1080,
83
+ components: [{ refid: 1, type: 'rect' }]
84
+ };
85
+ const input = buildChatMutationInput({
86
+ history: [],
87
+ liveBoard: board,
88
+ selectedRefids: []
89
+ });
90
+ expect(input.currentBoard).toBe(board);
91
+ });
92
+ test('scopes / knownTypes / categories 누락 시 undefined', () => {
93
+ const input = buildChatMutationInput({
94
+ history: [],
95
+ liveBoard: null,
96
+ selectedRefids: []
97
+ });
98
+ expect(input.scopes).toBeUndefined();
99
+ expect(input.knownTypes).toBeUndefined();
100
+ expect(input.categories).toBeUndefined();
101
+ expect(input.componentSchemas).toBeUndefined();
102
+ });
103
+ test('selectedRefids 다중 항목 (number 배열, stringify 없음)', () => {
104
+ const input = buildChatMutationInput({
105
+ history: [],
106
+ liveBoard: null,
107
+ selectedRefids: [35, 127, 4096]
108
+ });
109
+ expect(input.selectedRefids).toEqual([35, 127, 4096]);
110
+ });
111
+ });
112
+ //# sourceMappingURL=board-ai-chat.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-ai-chat.test.js","sourceRoot":"","sources":["../../client/components/board-ai-chat.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-ai-chat.ts 의 pure helper 회귀 방지.\n *\n * 핵심 보호:\n * - GraphQL mutation input 에 selectedRefids 가 항상 포함되는지 — 누락이 사용자\n * 보고된 \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n * - 식별자 정책 — refid (universal numeric) 가 selection 채널의 표준. id 는 데이터\n * 바인딩 이름이며 unique 가 아니라 selection 에 부적합.\n */\nimport { buildChatMutationInput } from './chat-input-builder'\n\ndescribe('buildChatMutationInput — mutation input 회귀 방지', () => {\n test('selectedRefids 가 input 에 반드시 포함된다 (root cause regression)', () => {\n const input = buildChatMutationInput({\n sessionId: 'session-1',\n history: [{ role: 'user', content: 'hi' }],\n liveBoard: { width: 1000, height: 600, components: [] },\n selectedRefids: [35]\n })\n expect(input).toHaveProperty('selectedRefids', [35])\n })\n\n test('selectedRefids 빈 배열도 누락 없이 포함', () => {\n const input = buildChatMutationInput({\n history: [],\n liveBoard: null,\n selectedRefids: []\n })\n expect(input.selectedRefids).toEqual([])\n })\n\n test('모든 expected 필드 존재', () => {\n const input = buildChatMutationInput({\n sessionId: 's1',\n history: [{ role: 'user', content: 'hi' }],\n liveBoard: { components: [] },\n scopes: ['fmsim'],\n knownTypes: ['rect'],\n categories: ['equipment'],\n componentSchemas: [{ type: 'rect' }],\n selectedRefids: [1, 2]\n })\n expect(Object.keys(input).sort()).toEqual([\n 'categories',\n 'componentSchemas',\n 'currentBoard',\n 'knownTypes',\n 'messages',\n 'scopes',\n 'selectedRefids',\n 'sessionId'\n ])\n })\n\n test('sessionId 누락 → null 정규화', () => {\n const input = buildChatMutationInput({\n history: [],\n liveBoard: null,\n selectedRefids: []\n })\n expect(input.sessionId).toBeNull()\n })\n\n test('liveBoard 누락 → null 정규화', () => {\n const input = buildChatMutationInput({\n history: [],\n liveBoard: undefined,\n selectedRefids: []\n })\n expect(input.currentBoard).toBeNull()\n })\n\n test('messages 가 history 그대로', () => {\n const history = [\n { role: 'user' as const, content: 'msg1' },\n { role: 'assistant' as const, content: 'reply1' }\n ]\n const input = buildChatMutationInput({\n history,\n liveBoard: null,\n selectedRefids: []\n })\n expect(input.messages).toBe(history)\n })\n\n test('liveBoard 가 그대로 currentBoard 로 매핑', () => {\n const board = {\n width: 1920,\n height: 1080,\n components: [{ refid: 1, type: 'rect' }]\n }\n const input = buildChatMutationInput({\n history: [],\n liveBoard: board,\n selectedRefids: []\n })\n expect(input.currentBoard).toBe(board)\n })\n\n test('scopes / knownTypes / categories 누락 시 undefined', () => {\n const input = buildChatMutationInput({\n history: [],\n liveBoard: null,\n selectedRefids: []\n })\n expect(input.scopes).toBeUndefined()\n expect(input.knownTypes).toBeUndefined()\n expect(input.categories).toBeUndefined()\n expect(input.componentSchemas).toBeUndefined()\n })\n\n test('selectedRefids 다중 항목 (number 배열, stringify 없음)', () => {\n const input = buildChatMutationInput({\n history: [],\n liveBoard: null,\n selectedRefids: [35, 127, 4096]\n })\n expect(input.selectedRefids).toEqual([35, 127, 4096])\n })\n})\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * boardAIChat GraphQL mutation 의 input 객체 빌더.
3
+ *
4
+ * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node
5
+ * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.
6
+ */
7
+ export interface ChatMutationInputArgs {
8
+ sessionId?: string;
9
+ history: Array<{
10
+ role: string;
11
+ content: string;
12
+ }>;
13
+ liveBoard: any;
14
+ scopes?: string[];
15
+ knownTypes?: string[];
16
+ categories?: string[];
17
+ componentSchemas?: any;
18
+ /**
19
+ * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.
20
+ * id (데이터 바인딩 이름) 와는 다른 개념.
21
+ */
22
+ selectedRefids: number[];
23
+ }
24
+ /**
25
+ * GraphQL mutation 의 input 객체를 빌드.
26
+ *
27
+ * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된
28
+ * "AI 가 선택된 컴포넌트를 모른다" 의 root cause 였다.
29
+ */
30
+ export declare function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * boardAIChat GraphQL mutation 의 input 객체 빌더.
3
+ *
4
+ * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node
5
+ * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.
6
+ */
7
+ /**
8
+ * GraphQL mutation 의 input 객체를 빌드.
9
+ *
10
+ * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된
11
+ * "AI 가 선택된 컴포넌트를 모른다" 의 root cause 였다.
12
+ */
13
+ export function buildChatMutationInput(args) {
14
+ return {
15
+ sessionId: args.sessionId ?? null,
16
+ messages: args.history,
17
+ currentBoard: args.liveBoard ?? null,
18
+ scopes: args.scopes,
19
+ knownTypes: args.knownTypes,
20
+ categories: args.categories,
21
+ componentSchemas: args.componentSchemas,
22
+ selectedRefids: args.selectedRefids
23
+ };
24
+ }
25
+ //# sourceMappingURL=chat-input-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-input-builder.js","sourceRoot":"","sources":["../../client/components/chat-input-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA2B;IAChE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAA;AACH,CAAC","sourcesContent":["/**\n * boardAIChat GraphQL mutation 의 input 객체 빌더.\n *\n * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node\n * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.\n */\n\nexport interface ChatMutationInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n liveBoard: any\n scopes?: string[]\n knownTypes?: string[]\n categories?: string[]\n componentSchemas?: any\n /**\n * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.\n * id (데이터 바인딩 이름) 와는 다른 개념.\n */\n selectedRefids: number[]\n}\n\n/**\n * GraphQL mutation 의 input 객체를 빌드.\n *\n * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된\n * \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n */\nexport function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n currentBoard: args.liveBoard ?? null,\n scopes: args.scopes,\n knownTypes: args.knownTypes,\n categories: args.categories,\n componentSchemas: args.componentSchemas,\n selectedRefids: args.selectedRefids\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ export declare function escapeHtml(text: string): string;
2
+ interface ExtractResult {
3
+ processed: string;
4
+ stored: string[];
5
+ }
6
+ /**
7
+ * `**X**` 패턴 중 CJK + 구두점 인접 때문에 marked 의 flanking 규칙에서 누락되는
8
+ * 케이스를 추출. 코드 펜스(```)와 인라인 코드(`...`) 안쪽은 건드리지 않는다.
9
+ *
10
+ * 검출 기준 (CommonMark right-flanking 실패 ↔ left-flanking 실패):
11
+ * - 닫는 ** 직전 문자가 구두점 AND 직후 문자가 CJK letter
12
+ * - 여는 ** 직전 문자가 CJK letter AND 직후 문자가 구두점
13
+ */
14
+ export declare function extractCjkStrongs(text: string): ExtractResult;
15
+ export declare function renderMarkdown(text: string): string;
16
+ export {};
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Board AI 채팅 메시지의 Markdown 렌더링.
3
+ *
4
+ * 보안:
5
+ * - raw HTML 차단 (renderer.html → ''), 위험 link 스킴 (javascript:, data: 등) 차단.
6
+ *
7
+ * CJK 보강:
8
+ * - CommonMark의 left/right-flanking 규칙 때문에 한국어/일본어/중국어 텍스트와
9
+ * ** 강조 마커 사이에 구두점이 끼면 마커가 닫히지 않는 케이스가 발생한다.
10
+ * 예) "**'rect'**의" → 닫는 ** 가 [구두점] + [CJK] 사이에 위치하므로 right-flanking 실패
11
+ * - 이런 케이스는 marked 에 넘기기 전에 자체적으로 추출 → 자리표시자로 치환 → marked
12
+ * 수행 → 후처리에서 <strong>...</strong> 로 복원하는 방식으로 처리한다.
13
+ * - 이 우회는 사용자가 의도적으로 넣은 ** 만 대상으로 하므로 보안적으로 안전하다 (내용은
14
+ * escapeHtml 로 항상 escape).
15
+ */
16
+ import { marked } from 'marked';
17
+ const markdownRenderer = new marked.Renderer();
18
+ markdownRenderer.html = () => '';
19
+ const origLink = markdownRenderer.link.bind(markdownRenderer);
20
+ markdownRenderer.link = (token) => {
21
+ const href = typeof token?.href === 'string' ? token.href.trim() : '';
22
+ if (/^(javascript|data|vbscript):/i.test(href)) {
23
+ return token?.text ?? '';
24
+ }
25
+ return origLink(token);
26
+ };
27
+ marked.setOptions({
28
+ gfm: true,
29
+ breaks: true,
30
+ pedantic: false,
31
+ renderer: markdownRenderer,
32
+ async: false
33
+ });
34
+ export function escapeHtml(text) {
35
+ return text
36
+ .replace(/&/g, '&amp;')
37
+ .replace(/</g, '&lt;')
38
+ .replace(/>/g, '&gt;')
39
+ .replace(/"/g, '&quot;')
40
+ .replace(/'/g, '&#039;');
41
+ }
42
+ // CJK 문자 범위: Hiragana, Katakana, CJK Unified Ideographs (Ext A 포함), Hangul, 전각/반각
43
+ const CJK_RE = /[぀-ヿ㐀-䶿一-鿿가-힯＀-￯]/;
44
+ const PUNCT_RE = /\p{P}/u;
45
+ // marked 가 절대 변형하지 않는 토큰 (영숫자만 사용)
46
+ const PLACEHOLDER_PREFIX = 'BAICJKSTRONG';
47
+ const PLACEHOLDER_SUFFIX = 'ENDMARK';
48
+ /**
49
+ * `**X**` 패턴 중 CJK + 구두점 인접 때문에 marked 의 flanking 규칙에서 누락되는
50
+ * 케이스를 추출. 코드 펜스(```)와 인라인 코드(`...`) 안쪽은 건드리지 않는다.
51
+ *
52
+ * 검출 기준 (CommonMark right-flanking 실패 ↔ left-flanking 실패):
53
+ * - 닫는 ** 직전 문자가 구두점 AND 직후 문자가 CJK letter
54
+ * - 여는 ** 직전 문자가 CJK letter AND 직후 문자가 구두점
55
+ */
56
+ export function extractCjkStrongs(text) {
57
+ const stored = [];
58
+ const lines = text.split('\n');
59
+ const outLines = [];
60
+ let inFence = false;
61
+ for (const line of lines) {
62
+ if (/^\s{0,3}```/.test(line)) {
63
+ inFence = !inFence;
64
+ outLines.push(line);
65
+ continue;
66
+ }
67
+ if (inFence) {
68
+ outLines.push(line);
69
+ continue;
70
+ }
71
+ outLines.push(processLine(line, stored, () => outLines[outLines.length - 1] ?? ''));
72
+ }
73
+ return { processed: outLines.join('\n'), stored };
74
+ }
75
+ function processLine(line, stored, prevLine) {
76
+ let i = 0;
77
+ let result = '';
78
+ while (i < line.length) {
79
+ const ch = line[i];
80
+ if (ch === '`') {
81
+ const end = line.indexOf('`', i + 1);
82
+ if (end === -1) {
83
+ result += line.slice(i);
84
+ break;
85
+ }
86
+ result += line.slice(i, end + 1);
87
+ i = end + 1;
88
+ continue;
89
+ }
90
+ if (ch === '*' && line[i + 1] === '*') {
91
+ const close = findCloseStrong(line, i + 2);
92
+ if (close === -1) {
93
+ result += '**';
94
+ i += 2;
95
+ continue;
96
+ }
97
+ const inner = line.slice(i + 2, close);
98
+ if (inner.length === 0) {
99
+ result += '**';
100
+ i += 2;
101
+ continue;
102
+ }
103
+ const before = result.length > 0 ? result.slice(-1) : prevLine().slice(-1);
104
+ const after = close + 2 < line.length ? line[close + 2] : '';
105
+ if (needsCjkFix(before, after, inner)) {
106
+ stored.push(inner);
107
+ result += `${PLACEHOLDER_PREFIX}${stored.length - 1}${PLACEHOLDER_SUFFIX}`;
108
+ }
109
+ else {
110
+ result += line.slice(i, close + 2);
111
+ }
112
+ i = close + 2;
113
+ continue;
114
+ }
115
+ result += ch;
116
+ i++;
117
+ }
118
+ return result;
119
+ }
120
+ function findCloseStrong(line, start) {
121
+ let i = start;
122
+ while (i < line.length - 1) {
123
+ if (line[i] === '`') {
124
+ const end = line.indexOf('`', i + 1);
125
+ if (end === -1)
126
+ return -1;
127
+ i = end + 1;
128
+ continue;
129
+ }
130
+ if (line[i] === '*' && line[i + 1] === '*')
131
+ return i;
132
+ i++;
133
+ }
134
+ return -1;
135
+ }
136
+ function needsCjkFix(before, after, inner) {
137
+ const innerStart = inner[0];
138
+ const innerEnd = inner[inner.length - 1];
139
+ return ((before !== '' && CJK_RE.test(before) && PUNCT_RE.test(innerStart)) ||
140
+ (after !== '' && CJK_RE.test(after) && PUNCT_RE.test(innerEnd)));
141
+ }
142
+ function restoreCjkStrongs(html, stored) {
143
+ let out = html;
144
+ for (let n = 0; n < stored.length; n++) {
145
+ const token = `${PLACEHOLDER_PREFIX}${n}${PLACEHOLDER_SUFFIX}`;
146
+ const replacement = `<strong>${escapeHtml(stored[n])}</strong>`;
147
+ out = out.split(token).join(replacement);
148
+ }
149
+ return out;
150
+ }
151
+ export function renderMarkdown(text) {
152
+ if (!text)
153
+ return '';
154
+ try {
155
+ const { processed, stored } = extractCjkStrongs(text);
156
+ const out = marked.parse(processed);
157
+ if (typeof out !== 'string') {
158
+ // marked 가 Promise 를 반환했다면 (async lexer 등) 안전한 fallback
159
+ return escapeHtml(text).replace(/\n/g, '<br>');
160
+ }
161
+ return restoreCjkStrongs(out, stored);
162
+ }
163
+ catch {
164
+ return escapeHtml(text).replace(/\n/g, '<br>');
165
+ }
166
+ }
167
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../client/components/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Board AI 채팅 메시지의 Markdown 렌더링.\n *\n * 보안:\n * - raw HTML 차단 (renderer.html → ''), 위험 link 스킴 (javascript:, data: 등) 차단.\n *\n * CJK 보강:\n * - CommonMark의 left/right-flanking 규칙 때문에 한국어/일본어/중국어 텍스트와\n * ** 강조 마커 사이에 구두점이 끼면 마커가 닫히지 않는 케이스가 발생한다.\n * 예) \"**'rect'**의\" → 닫는 ** 가 [구두점] + [CJK] 사이에 위치하므로 right-flanking 실패\n * - 이런 케이스는 marked 에 넘기기 전에 자체적으로 추출 → 자리표시자로 치환 → marked\n * 수행 → 후처리에서 <strong>...</strong> 로 복원하는 방식으로 처리한다.\n * - 이 우회는 사용자가 의도적으로 넣은 ** 만 대상으로 하므로 보안적으로 안전하다 (내용은\n * escapeHtml 로 항상 escape).\n */\nimport { marked } from 'marked'\n\nconst markdownRenderer = new marked.Renderer()\nmarkdownRenderer.html = () => ''\nconst origLink = markdownRenderer.link.bind(markdownRenderer)\nmarkdownRenderer.link = (token: any) => {\n const href = typeof token?.href === 'string' ? token.href.trim() : ''\n if (/^(javascript|data|vbscript):/i.test(href)) {\n return token?.text ?? ''\n }\n return origLink(token)\n}\n\nmarked.setOptions({\n gfm: true,\n breaks: true,\n pedantic: false,\n renderer: markdownRenderer,\n async: false\n} as any)\n\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;')\n}\n\n// CJK 문자 범위: Hiragana, Katakana, CJK Unified Ideographs (Ext A 포함), Hangul, 전각/반각\nconst CJK_RE = /[぀-ヿ㐀-䶿一-鿿가-힯＀-￯]/\nconst PUNCT_RE = /\\p{P}/u\n\n// marked 가 절대 변형하지 않는 토큰 (영숫자만 사용)\nconst PLACEHOLDER_PREFIX = 'BAICJKSTRONG'\nconst PLACEHOLDER_SUFFIX = 'ENDMARK'\n\ninterface ExtractResult {\n processed: string\n stored: string[]\n}\n\n/**\n * `**X**` 패턴 중 CJK + 구두점 인접 때문에 marked 의 flanking 규칙에서 누락되는\n * 케이스를 추출. 코드 펜스(```)와 인라인 코드(`...`) 안쪽은 건드리지 않는다.\n *\n * 검출 기준 (CommonMark right-flanking 실패 ↔ left-flanking 실패):\n * - 닫는 ** 직전 문자가 구두점 AND 직후 문자가 CJK letter\n * - 여는 ** 직전 문자가 CJK letter AND 직후 문자가 구두점\n */\nexport function extractCjkStrongs(text: string): ExtractResult {\n const stored: string[] = []\n const lines = text.split('\\n')\n const outLines: string[] = []\n let inFence = false\n\n for (const line of lines) {\n if (/^\\s{0,3}```/.test(line)) {\n inFence = !inFence\n outLines.push(line)\n continue\n }\n if (inFence) {\n outLines.push(line)\n continue\n }\n outLines.push(processLine(line, stored, () => outLines[outLines.length - 1] ?? ''))\n }\n\n return { processed: outLines.join('\\n'), stored }\n}\n\nfunction processLine(line: string, stored: string[], prevLine: () => string): string {\n let i = 0\n let result = ''\n while (i < line.length) {\n const ch = line[i]\n\n if (ch === '`') {\n const end = line.indexOf('`', i + 1)\n if (end === -1) {\n result += line.slice(i)\n break\n }\n result += line.slice(i, end + 1)\n i = end + 1\n continue\n }\n\n if (ch === '*' && line[i + 1] === '*') {\n const close = findCloseStrong(line, i + 2)\n if (close === -1) {\n result += '**'\n i += 2\n continue\n }\n const inner = line.slice(i + 2, close)\n if (inner.length === 0) {\n result += '**'\n i += 2\n continue\n }\n const before = result.length > 0 ? result.slice(-1) : prevLine().slice(-1)\n const after = close + 2 < line.length ? line[close + 2] : ''\n if (needsCjkFix(before, after, inner)) {\n stored.push(inner)\n result += `${PLACEHOLDER_PREFIX}${stored.length - 1}${PLACEHOLDER_SUFFIX}`\n } else {\n result += line.slice(i, close + 2)\n }\n i = close + 2\n continue\n }\n\n result += ch\n i++\n }\n return result\n}\n\nfunction findCloseStrong(line: string, start: number): number {\n let i = start\n while (i < line.length - 1) {\n if (line[i] === '`') {\n const end = line.indexOf('`', i + 1)\n if (end === -1) return -1\n i = end + 1\n continue\n }\n if (line[i] === '*' && line[i + 1] === '*') return i\n i++\n }\n return -1\n}\n\nfunction needsCjkFix(before: string, after: string, inner: string): boolean {\n const innerStart = inner[0]\n const innerEnd = inner[inner.length - 1]\n return (\n (before !== '' && CJK_RE.test(before) && PUNCT_RE.test(innerStart)) ||\n (after !== '' && CJK_RE.test(after) && PUNCT_RE.test(innerEnd))\n )\n}\n\nfunction restoreCjkStrongs(html: string, stored: string[]): string {\n let out = html\n for (let n = 0; n < stored.length; n++) {\n const token = `${PLACEHOLDER_PREFIX}${n}${PLACEHOLDER_SUFFIX}`\n const replacement = `<strong>${escapeHtml(stored[n])}</strong>`\n out = out.split(token).join(replacement)\n }\n return out\n}\n\nexport function renderMarkdown(text: string): string {\n if (!text) return ''\n try {\n const { processed, stored } = extractCjkStrongs(text)\n const out = marked.parse(processed)\n if (typeof out !== 'string') {\n // marked 가 Promise 를 반환했다면 (async lexer 등) 안전한 fallback\n return escapeHtml(text).replace(/\\n/g, '<br>')\n }\n return restoreCjkStrongs(out, stored)\n } catch {\n return escapeHtml(text).replace(/\\n/g, '<br>')\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};