@iyulab/chat-components 0.13.0 → 0.14.1

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 (64) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -0
  3. package/dist/_virtual/_glob-assets_raw/alert-triangle-fill.svg.4cfc4305.js +4 -0
  4. package/dist/_virtual/_glob-assets_raw/arrows-sort.svg.dda43a85.js +4 -0
  5. package/dist/_virtual/_glob-assets_raw/code.svg.c69bc144.js +4 -0
  6. package/dist/_virtual/_glob-assets_raw/download.svg.bce82177.js +4 -0
  7. package/dist/_virtual/_glob-assets_raw/external-link.svg.a9f7f02d.js +4 -0
  8. package/dist/_virtual/_glob-assets_raw/file-code.svg.0e706f93.js +4 -0
  9. package/dist/_virtual/_glob-assets_raw/file-music.svg.b9970203.js +4 -0
  10. package/dist/_virtual/_glob-assets_raw/file-spreadsheet.svg.8c03bdfa.js +4 -0
  11. package/dist/_virtual/_glob-assets_raw/file-text.svg.85bdd923.js +4 -0
  12. package/dist/_virtual/_glob-assets_raw/file-type-pdf.svg.bc12db6d.js +4 -0
  13. package/dist/_virtual/_glob-assets_raw/file-zip.svg.76dde660.js +4 -0
  14. package/dist/_virtual/_glob-assets_raw/file.svg.28f5d6f1.js +4 -0
  15. package/dist/_virtual/_glob-assets_raw/photo.svg.cc273cc1.js +4 -0
  16. package/dist/_virtual/_glob-assets_raw/player-stop-fill.svg.5378b752.js +4 -0
  17. package/dist/_virtual/_glob-assets_raw/send.svg.02326253.js +4 -0
  18. package/dist/_virtual/_glob-assets_raw/sort-ascending-letters.svg.14a3df67.js +4 -0
  19. package/dist/_virtual/_glob-assets_raw/sort-descending-letters.svg.3e03647c.js +4 -0
  20. package/dist/_virtual/_glob-assets_raw/video.svg.c183f03c.js +4 -0
  21. package/dist/_virtual/_glob-assets_raw/world.svg.cd9903c8.js +4 -0
  22. package/dist/components/UDataElement.d.ts +2 -0
  23. package/dist/components/UDataElement.js +9 -1
  24. package/dist/components/blocks/UCodeBlock.js +1 -0
  25. package/dist/components/blocks/UElementBlock.d.ts +12 -16
  26. package/dist/components/blocks/UElementBlock.js +23 -31
  27. package/dist/components/blocks/UFileBlock.js +1 -0
  28. package/dist/components/blocks/UMarkedBlock.d.ts +10 -1
  29. package/dist/components/blocks/UMarkedBlock.js +43 -13
  30. package/dist/components/blocks/UMarkedBlock.styles.js +10 -0
  31. package/dist/components/blocks/UTableBlock.js +1 -0
  32. package/dist/components/prompt/UPrompt.js +3 -1
  33. package/dist/components/references/URefCard.js +3 -1
  34. package/dist/components/references/URefTag.js +3 -1
  35. package/dist/components-extra/UChartBlock.js +4 -0
  36. package/dist/components-extra/UImagesBlock.d.ts +3 -0
  37. package/dist/components-extra/UImagesBlock.js +18 -3
  38. package/dist/components-extra/UImagesBlock.styles.js +19 -0
  39. package/dist/components-extra/UMapBlock.js +3 -0
  40. package/dist/components-extra/UVideoBlock.js +3 -0
  41. package/dist/extra.d.ts +7 -1
  42. package/dist/extra.js +12 -6
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +5 -2
  45. package/dist/utilities/ElementRegistry.d.ts +13 -0
  46. package/dist/utilities/ElementRegistry.js +33 -0
  47. package/dist/utilities/HtmlPlaceholder.d.ts +8 -0
  48. package/dist/utilities/HtmlPlaceholder.js +13 -1
  49. package/dist/utilities/PromptBuilder.d.ts +3 -0
  50. package/dist/utilities/PromptBuilder.js +5 -0
  51. package/dist/utilities/icons.js +52 -0
  52. package/dist/utilities/imageSources.d.ts +25 -0
  53. package/dist/utilities/imageSources.js +61 -0
  54. package/dist/utilities/messages.d.ts +1 -1
  55. package/dist/utilities/messages.js +6 -2
  56. package/dist/utilities/sanitizers.d.ts +10 -1
  57. package/dist/utilities/sanitizers.js +16 -5
  58. package/package.json +4 -2
  59. package/skills/iyulab-chat-components/SKILL.md +1 -0
  60. package/skills/iyulab-chat-components/references/components/element-block.md +5 -6
  61. package/skills/iyulab-chat-components/references/components/images-block.md +1 -0
  62. package/skills/iyulab-chat-components/references/components/marked-block.md +17 -0
  63. package/skills/iyulab-chat-components/references/extra-system.md +4 -4
  64. package/skills/iyulab-chat-components/references/security.md +23 -0
@@ -1,3 +1,4 @@
1
+ import "../../utilities/icons.js";
1
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
4
  import { messages } from "../../utilities/messages.js";
@@ -9,6 +10,7 @@ import "@iyulab/components/dist/components/icon/UIcon.js";
9
10
  import { UElement } from "@iyulab/components/dist/components/UElement.js";
10
11
  import "@iyulab/components/dist/components/button/UButton.js";
11
12
  import { repeat } from "lit/directives/repeat.js";
13
+ import { isImeComposing } from "@iyulab/components/dist/utilities/keyboard.js";
12
14
  //#region src/components/prompt/UPrompt.ts
13
15
  var UPrompt = class UPrompt extends UElement {
14
16
  constructor(..._args) {
@@ -115,7 +117,7 @@ var UPrompt = class UPrompt extends UElement {
115
117
  this.value = target.value;
116
118
  }
117
119
  handleTextBlockKeydown(e) {
118
- if (e.key === "Enter" && !e.shiftKey) {
120
+ if (e.key === "Enter" && !e.shiftKey && !isImeComposing(e)) {
119
121
  e.preventDefault();
120
122
  this.submit();
121
123
  }
@@ -1,5 +1,7 @@
1
+ import "../../utilities/icons.js";
1
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
4
+ import { sanitizeHref } from "../../utilities/sanitizers.js";
3
5
  import { UDataElement } from "../UDataElement.js";
4
6
  import { styles } from "./URefCard.styles.js";
5
7
  import { html, nothing } from "lit";
@@ -36,7 +38,7 @@ var URefCard = class URefCard extends UDataElement {
36
38
  }
37
39
  render() {
38
40
  return html`
39
- <a href="${ifDefined(this.url)}" target="_blank" rel="noopener noreferrer"
41
+ <a href="${ifDefined(this.url ? sanitizeHref(this.url) : void 0)}" target="_blank" rel="noopener noreferrer"
40
42
  @click=${this.handleAnchorClick}>
41
43
  <div class="header">
42
44
  ${this.renderFavicon()}
@@ -1,5 +1,7 @@
1
+ import "../../utilities/icons.js";
1
2
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
4
+ import { sanitizeHref } from "../../utilities/sanitizers.js";
3
5
  import { styles } from "./URefTag.styles.js";
4
6
  import { html } from "lit";
5
7
  import { customElement, property } from "lit/decorators.js";
@@ -14,7 +16,7 @@ var URefTag = class URefTag extends UElement {
14
16
  }
15
17
  render() {
16
18
  return html`
17
- <a href="${ifDefined(this.href)}" target="_blank" rel="noopener noreferrer"
19
+ <a href="${ifDefined(this.href ? sanitizeHref(this.href) : void 0)}" target="_blank" rel="noopener noreferrer"
18
20
  @click=${this.handleAnchorClick}>
19
21
  <slot></slot>
20
22
  </a>
@@ -1,7 +1,10 @@
1
+ import "../utilities/icons.js";
1
2
  import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
3
  import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
4
+ import { registerElementBlock } from "../utilities/ElementRegistry.js";
3
5
  import { messages } from "../utilities/messages.js";
4
6
  import { styles } from "./UChartBlock.styles.js";
7
+ import schema from "./UChartBlock.schema.js";
5
8
  import { html, nothing } from "lit";
6
9
  import { customElement, property, query, state } from "lit/decorators.js";
7
10
  import "@iyulab/components/dist/components/icon/UIcon.js";
@@ -9,6 +12,7 @@ import { UElement } from "@iyulab/components/dist/components/UElement.js";
9
12
  import "@iyulab/components/dist/components/button/UButton.js";
10
13
  import { Chart } from "chart.js/auto";
11
14
  //#region src/components-extra/UChartBlock.ts
15
+ registerElementBlock(schema);
12
16
  var UChartBlock = class UChartBlock extends UElement {
13
17
  constructor(..._args) {
14
18
  super(..._args);
@@ -19,6 +19,9 @@ export declare class UImagesBlock extends UElement {
19
19
  /** 열린 라이트박스는 층이다 — Escape 는 components 의 층 스택이 «가장 나중에 연 층 하나» 에만 준다. */
20
20
  protected updated(changed: PropertyValues): void;
21
21
  render(): typeof nothing | import('lit-html').TemplateResult<1>;
22
+ /** 출처 정책(`setAllowedImagePrefixes`)이 막은 이미지 자리의 문구. */
23
+ private blockedLabel;
24
+ private renderLightboxImage;
22
25
  private renderLightbox;
23
26
  open: (i: number) => void;
24
27
  close: () => void;
@@ -1,7 +1,10 @@
1
1
  import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
2
  import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
+ import { registerElementBlock } from "../utilities/ElementRegistry.js";
3
4
  import { messages } from "../utilities/messages.js";
5
+ import { resolveImageSource } from "../utilities/imageSources.js";
4
6
  import { styles } from "./UImagesBlock.styles.js";
7
+ import schema from "./UImagesBlock.schema.js";
5
8
  import { html, nothing } from "lit";
6
9
  import { customElement, property, state } from "lit/decorators.js";
7
10
  import "@iyulab/components/dist/components/icon/UIcon.js";
@@ -10,6 +13,7 @@ import { OverlayManager } from "@iyulab/components/dist/utilities/OverlayManager
10
13
  import { repeat } from "lit/directives/repeat.js";
11
14
  import "@iyulab/components/dist/components/carousel/UCarousel.js";
12
15
  //#region src/components-extra/UImagesBlock.ts
16
+ registerElementBlock(schema);
13
17
  var UImagesBlock = class UImagesBlock extends UElement {
14
18
  constructor(..._args) {
15
19
  super(..._args);
@@ -72,14 +76,17 @@ var UImagesBlock = class UImagesBlock extends UElement {
72
76
  .slidesPerView=${Math.min(count, 3)}
73
77
  .gap=${8}
74
78
  >
75
- ${repeat(this.items, (item, i) => html`
79
+ ${repeat(this.items, (item, i) => resolveImageSource(item.src) === null ? html`
80
+ <!-- 출처 정책이 막은 이미지는 요청하지 않는다 — 열 것이 없으므로 버튼도 아니다. -->
81
+ <div class="slide blocked">${this.blockedLabel(item)}</div>
82
+ ` : html`
76
83
  <!-- 썸네일은 라이트박스를 여는 버튼이다 — 클릭만 받는 div 는 키보드로 열 수 없었다.
77
84
  대체 텍스트가 있으면 그것이 이름이고, 없으면 «이미지 N 열기» 로 이름을 준다. -->
78
85
  <button type="button" class="slide"
79
86
  aria-label=${item.alt ? nothing : messages.text("openImage", { index: i + 1 })}
80
87
  @click=${() => this.open(i)}>
81
88
  <img
82
- src=${item.src}
89
+ src=${resolveImageSource(item.src)}
83
90
  alt=${item.alt || ""}
84
91
  loading="lazy"
85
92
  />
@@ -94,6 +101,14 @@ var UImagesBlock = class UImagesBlock extends UElement {
94
101
  ${this.renderLightbox()}
95
102
  `;
96
103
  }
104
+ /** 출처 정책(`setAllowedImagePrefixes`)이 막은 이미지 자리의 문구. */
105
+ blockedLabel(item) {
106
+ return item.alt ? messages.text("imageBlockedNamed", { name: item.alt }) : messages.text("imageBlocked");
107
+ }
108
+ renderLightboxImage(img) {
109
+ const src = resolveImageSource(img.src);
110
+ return src === null ? html`<div class="lb-blocked">${this.blockedLabel(img)}</div>` : html`<img src=${src} alt=${img.alt || ""} />`;
111
+ }
97
112
  renderLightbox() {
98
113
  const idx = this.index;
99
114
  if (idx === null) return nothing;
@@ -121,7 +136,7 @@ var UImagesBlock = class UImagesBlock extends UElement {
121
136
  <div class="lb-track" style="transform:translateX(${translateX})">
122
137
  ${repeat(this.items, (img, i) => html`
123
138
  <div class="lb-slide" ?active=${i === idx}>
124
- <img src=${img.src} alt=${img.alt || ""} />
139
+ ${this.renderLightboxImage(img)}
125
140
  </div>
126
141
  `)}
127
142
  </div>
@@ -40,6 +40,19 @@ var styles = css`
40
40
  filter: brightness(0.92);
41
41
  }
42
42
 
43
+ /* 출처 정책이 막은 이미지 — 요청 없이 대체 텍스트만. */
44
+ .slide.blocked {
45
+ display: grid;
46
+ place-items: center;
47
+ padding: 8px;
48
+ box-sizing: border-box;
49
+ border: 1px dashed var(--u-border-color, #E0E0E0);
50
+ color: var(--u-txt-color-weak, #616161);
51
+ font-size: 0.8em;
52
+ text-align: center;
53
+ cursor: default;
54
+ }
55
+
43
56
  .caption {
44
57
  position: absolute;
45
58
  bottom: 0;
@@ -160,6 +173,12 @@ var styles = css`
160
173
  user-select: none;
161
174
  pointer-events: none;
162
175
  }
176
+ .lb-blocked {
177
+ padding: 16px 24px;
178
+ border: 1px dashed rgba(255, 255, 255, 0.5);
179
+ border-radius: 6px;
180
+ color: white;
181
+ }
163
182
 
164
183
  .lb-nav {
165
184
  position: absolute;
@@ -1,10 +1,13 @@
1
1
  import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
2
  import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
+ import { registerElementBlock } from "../utilities/ElementRegistry.js";
3
4
  import { styles } from "./UMapBlock.styles.js";
5
+ import schema from "./UMapBlock.schema.js";
4
6
  import { html, nothing } from "lit";
5
7
  import { customElement, property } from "lit/decorators.js";
6
8
  import { UElement } from "@iyulab/components/dist/components/UElement.js";
7
9
  //#region src/components-extra/UMapBlock.ts
10
+ registerElementBlock(schema);
8
11
  var UMapBlock = class UMapBlock extends UElement {
9
12
  constructor(..._args) {
10
13
  super(..._args);
@@ -1,11 +1,14 @@
1
1
  import __decorateMetadata from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
2
  import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
+ import { registerElementBlock } from "../utilities/ElementRegistry.js";
3
4
  import { styles } from "./UVideoBlock.styles.js";
5
+ import schema from "./UVideoBlock.schema.js";
4
6
  import { html, nothing } from "lit";
5
7
  import { customElement, property } from "lit/decorators.js";
6
8
  import { UElement } from "@iyulab/components/dist/components/UElement.js";
7
9
  import { ifDefined } from "lit/directives/if-defined.js";
8
10
  //#region src/components-extra/UVideoBlock.ts
11
+ registerElementBlock(schema);
9
12
  var UVideoBlock = class UVideoBlock extends UElement {
10
13
  constructor(..._args) {
11
14
  super(..._args);
package/dist/extra.d.ts CHANGED
@@ -2,5 +2,11 @@ export * from './components-extra/UChartBlock.js';
2
2
  export * from './components-extra/UImagesBlock.js';
3
3
  export * from './components-extra/UMapBlock.js';
4
4
  export * from './components-extra/UVideoBlock.js';
5
- /** 내장 extra 블록(chart/images/map/video)이 모두 등록된 LLM 시스템 프롬프트 조각 */
5
+ /**
6
+ * 내장 extra 블록(chart/images/map/video)이 모두 등록된 LLM 시스템 프롬프트 조각.
7
+ *
8
+ * 공유 인스턴스(`ElementPromptBuilder.instance`)에 등록한다 — 문서가 약속하듯 소비자가 거기에
9
+ * 자기 extra 를 더한 뒤 `build()` 를 다시 부르면 내장 넷과 함께 나온다. 종전에는 별도 인스턴스에
10
+ * 등록해 그 `build()` 가 소비자 것만 돌려줬다.
11
+ */
6
12
  export declare const prompt: string;
package/dist/extra.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { ElementPromptBuilder } from "./utilities/PromptBuilder.js";
2
- import { UChartBlock } from "./components-extra/UChartBlock.js";
3
- import { UImagesBlock } from "./components-extra/UImagesBlock.js";
4
- import { UMapBlock } from "./components-extra/UMapBlock.js";
5
- import { UVideoBlock } from "./components-extra/UVideoBlock.js";
6
2
  import schema from "./components-extra/UChartBlock.schema.js";
3
+ import { UChartBlock } from "./components-extra/UChartBlock.js";
7
4
  import schema$1 from "./components-extra/UImagesBlock.schema.js";
5
+ import { UImagesBlock } from "./components-extra/UImagesBlock.js";
8
6
  import schema$2 from "./components-extra/UMapBlock.schema.js";
7
+ import { UMapBlock } from "./components-extra/UMapBlock.js";
9
8
  import schema$3 from "./components-extra/UVideoBlock.schema.js";
9
+ import { UVideoBlock } from "./components-extra/UVideoBlock.js";
10
10
  //#region src/extra.ts
11
- /** 내장 extra 블록(chart/images/map/video)이 모두 등록된 LLM 시스템 프롬프트 조각 */
12
- var prompt = new ElementPromptBuilder().add(schema$1).add(schema$3).add(schema$2).add(schema).build();
11
+ /**
12
+ * 내장 extra 블록(chart/images/map/video)이 모두 등록된 LLM 시스템 프롬프트 조각.
13
+ *
14
+ * 공유 인스턴스(`ElementPromptBuilder.instance`)에 등록한다 — 문서가 약속하듯 소비자가 거기에
15
+ * 자기 extra 를 더한 뒤 `build()` 를 다시 부르면 내장 넷과 함께 나온다. 종전에는 별도 인스턴스에
16
+ * 등록해 그 `build()` 가 소비자 것만 돌려줬다.
17
+ */
18
+ var prompt = ElementPromptBuilder.instance.add(schema$1).add(schema$3).add(schema$2).add(schema).build();
13
19
  //#endregion
14
20
  export { UChartBlock, UImagesBlock, UMapBlock, UVideoBlock, prompt };
package/dist/index.d.ts CHANGED
@@ -17,4 +17,6 @@ export type * from './events/SendEvent';
17
17
  export type * from './events/StopEvent';
18
18
  export * from './utilities/HtmlBuilder.js';
19
19
  export * from './utilities/PromptBuilder.js';
20
+ export * from './utilities/ElementRegistry.js';
20
21
  export * from './utilities/sanitizers.js';
22
+ export * from './utilities/imageSources.js';
package/dist/index.js CHANGED
@@ -1,16 +1,19 @@
1
+ import "./utilities/icons.js";
1
2
  import { UCodeBlock } from "./components/blocks/UCodeBlock.js";
2
- import { escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, stripZeroWidth } from "./utilities/sanitizers.js";
3
+ import { escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, sanitizeHref, stripZeroWidth } from "./utilities/sanitizers.js";
3
4
  import { HtmlBuilder } from "./utilities/HtmlBuilder.js";
5
+ import { getElementBlockProperties, registerElementBlock } from "./utilities/ElementRegistry.js";
4
6
  import { UElementBlock } from "./components/blocks/UElementBlock.js";
5
7
  import { UFileBlock } from "./components/blocks/UFileBlock.js";
6
8
  import { URefTag } from "./components/references/URefTag.js";
7
9
  import { URefCard, googleFaviconUrl } from "./components/references/URefCard.js";
8
10
  import { URefCardGroup } from "./components/references/URefCardGroup.js";
9
11
  import { UTableBlock } from "./components/blocks/UTableBlock.js";
12
+ import { getAllowedImagePrefixes, resolveImageSource, setAllowedImagePrefixes } from "./utilities/imageSources.js";
10
13
  import { UMarkedBlock } from "./components/blocks/UMarkedBlock.js";
11
14
  import { UTextBlock } from "./components/blocks/UTextBlock.js";
12
15
  import { URefBlock } from "./components/blocks/URefBlock.js";
13
16
  import { UMessage } from "./components/message/UMessage.js";
14
17
  import { UPrompt } from "./components/prompt/UPrompt.js";
15
18
  import { ElementPromptBuilder } from "./utilities/PromptBuilder.js";
16
- export { ElementPromptBuilder, HtmlBuilder, UCodeBlock, UElementBlock, UFileBlock, UMarkedBlock, UMessage, UPrompt, URefBlock, URefCard, URefCardGroup, URefTag, UTableBlock, UTextBlock, escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, googleFaviconUrl, stripZeroWidth };
19
+ export { ElementPromptBuilder, HtmlBuilder, UCodeBlock, UElementBlock, UFileBlock, UMarkedBlock, UMessage, UPrompt, URefBlock, URefCard, URefCardGroup, URefTag, UTableBlock, UTextBlock, escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, getAllowedImagePrefixes, getElementBlockProperties, googleFaviconUrl, registerElementBlock, resolveImageSource, sanitizeHref, setAllowedImagePrefixes, stripZeroWidth };
@@ -0,0 +1,13 @@
1
+ import { ElementSchema } from '../types/Schema.js';
2
+ /**
3
+ * 블록을 `block-json` 으로 렌더할 수 있게 등록합니다.
4
+ *
5
+ * 내장 블록(chart/images/map/video)은 모듈을 import 하면 스스로 등록되고,
6
+ * `ElementPromptBuilder.add()` 로 추가한 스키마도 함께 등록됩니다.
7
+ * 같은 태그를 다시 등록하면 속성 목록을 새 스키마로 바꿉니다.
8
+ */
9
+ export declare function registerElementBlock(schema: Pick<ElementSchema, 'tag' | 'properties'>): void;
10
+ /**
11
+ * 등록된 블록이면 대입을 허용하는 속성 이름 집합을, 아니면 `undefined` 를 돌려줍니다.
12
+ */
13
+ export declare function getElementBlockProperties(tag: string): ReadonlySet<string> | undefined;
@@ -0,0 +1,33 @@
1
+ //#region src/utilities/ElementRegistry.ts
2
+ /**
3
+ * `block-json` 코드펜스로 만들 수 있는 블록의 허용 목록.
4
+ *
5
+ * `block-json` 은 LLM 출력이다 — 프롬프트 인젝션(검색 문서·첨부·도구 결과)으로 제3자가 쓸 수
6
+ * 있는 입력이라, «등록된 커스텀 엘리먼트면 무엇이든 만든다 + 위험한 속성 이름만 막는다» 는
7
+ * 경계가 되지 못한다. 페이지에는 신뢰된 HTML 을 받는 속성을 가진 엘리먼트가 있고, 거부 목록은
8
+ * 그 이름을 모른다.
9
+ *
10
+ * ⇒ **LLM 에게 알린 스키마가 곧 허용 목록이다.** 태그는 스키마로 등록된 것만, 속성은 그
11
+ * 스키마의 `properties` 에 적힌 이름만 대입한다. 스키마는 이미 LLM 과의 계약이므로 두 번째
12
+ * 목록을 따로 유지할 필요가 없다.
13
+ */
14
+ var blocks = /* @__PURE__ */ new Map();
15
+ /**
16
+ * 블록을 `block-json` 으로 렌더할 수 있게 등록합니다.
17
+ *
18
+ * 내장 블록(chart/images/map/video)은 모듈을 import 하면 스스로 등록되고,
19
+ * `ElementPromptBuilder.add()` 로 추가한 스키마도 함께 등록됩니다.
20
+ * 같은 태그를 다시 등록하면 속성 목록을 새 스키마로 바꿉니다.
21
+ */
22
+ function registerElementBlock(schema) {
23
+ const tag = schema.tag.trim().toLowerCase();
24
+ blocks.set(tag, new Set(Object.keys(schema.properties ?? {})));
25
+ }
26
+ /**
27
+ * 등록된 블록이면 대입을 허용하는 속성 이름 집합을, 아니면 `undefined` 를 돌려줍니다.
28
+ */
29
+ function getElementBlockProperties(tag) {
30
+ return blocks.get(tag.trim().toLowerCase());
31
+ }
32
+ //#endregion
33
+ export { getElementBlockProperties, registerElementBlock };
@@ -11,6 +11,14 @@
11
11
  * 종전처럼 한쪽만 바뀌어 어긋난다.
12
12
  */
13
13
  export declare function stripRefPlaceholders(value: string): string;
14
+ /**
15
+ * 문자열에서 ref 플레이스홀더 센티널을 떼어 낸다 — `[센티널 없는 문자열, 떼어 낸 센티널들]`.
16
+ *
17
+ * 링크 주소·이미지 속성·인라인 코드처럼 **태그가 들어갈 수 없는 자리**에 인용이 떨어졌을 때 쓴다.
18
+ * 그 자리에 남기면 `restore()` 가 속성 값이나 `<code>` 안에 HTML 을 넣는다(링크가 깨지고 코드에
19
+ * 인용 태그가 섞인다). 지우면 인용을 잃으므로, 호출부가 떼어 낸 것을 그 구문 **바로 뒤**에 둔다.
20
+ */
21
+ export declare function extractRefPlaceholders(value: string): [string, string];
14
22
  export declare class HtmlPlaceholder {
15
23
  private map;
16
24
  private idx;
@@ -50,6 +50,18 @@ var sentinelPattern = () => /\u{E000}\d+\u{E001}/gu;
50
50
  function stripRefPlaceholders(value) {
51
51
  return value.replace(sentinelPattern(), "");
52
52
  }
53
+ /**
54
+ * 문자열에서 ref 플레이스홀더 센티널을 떼어 낸다 — `[센티널 없는 문자열, 떼어 낸 센티널들]`.
55
+ *
56
+ * 링크 주소·이미지 속성·인라인 코드처럼 **태그가 들어갈 수 없는 자리**에 인용이 떨어졌을 때 쓴다.
57
+ * 그 자리에 남기면 `restore()` 가 속성 값이나 `<code>` 안에 HTML 을 넣는다(링크가 깨지고 코드에
58
+ * 인용 태그가 섞인다). 지우면 인용을 잃으므로, 호출부가 떼어 낸 것을 그 구문 **바로 뒤**에 둔다.
59
+ */
60
+ function extractRefPlaceholders(value) {
61
+ const keys = value.match(sentinelPattern());
62
+ if (!keys) return [value, ""];
63
+ return [value.replace(sentinelPattern(), ""), keys.join("")];
64
+ }
53
65
  var HtmlPlaceholder = class {
54
66
  constructor() {
55
67
  this.map = {};
@@ -78,4 +90,4 @@ var HtmlPlaceholder = class {
78
90
  }
79
91
  };
80
92
  //#endregion
81
- export { HtmlPlaceholder, stripRefPlaceholders };
93
+ export { HtmlPlaceholder, extractRefPlaceholders, stripRefPlaceholders };
@@ -11,6 +11,9 @@ export declare class ElementPromptBuilder {
11
11
  static get instance(): ElementPromptBuilder;
12
12
  /**
13
13
  * Element 정의를 등록합니다. 같은 태그가 이미 등록되어 있으면 에러를 던집니다.
14
+ *
15
+ * LLM 에게 알리는 스키마가 곧 `u-element-block` 의 허용 목록이므로, 여기서 추가한 블록은
16
+ * `block-json` 으로도 렌더할 수 있게 함께 등록됩니다(`registerElementBlock`).
14
17
  * @throws {Error} 같은 태그가 이미 등록된 경우
15
18
  */
16
19
  add(schema: ElementSchema): ElementPromptBuilder;
@@ -1,3 +1,4 @@
1
+ import { registerElementBlock } from "./ElementRegistry.js";
1
2
  import element_prompt_md_default from "../_virtual/_glob-assets_raw/element-prompt.md.7047ffcc.js";
2
3
  //#region src/utilities/PromptBuilder.ts
3
4
  /**
@@ -16,11 +17,15 @@ var ElementPromptBuilder = class ElementPromptBuilder {
16
17
  }
17
18
  /**
18
19
  * Element 정의를 등록합니다. 같은 태그가 이미 등록되어 있으면 에러를 던집니다.
20
+ *
21
+ * LLM 에게 알리는 스키마가 곧 `u-element-block` 의 허용 목록이므로, 여기서 추가한 블록은
22
+ * `block-json` 으로도 렌더할 수 있게 함께 등록됩니다(`registerElementBlock`).
19
23
  * @throws {Error} 같은 태그가 이미 등록된 경우
20
24
  */
21
25
  add(schema) {
22
26
  if (this.elements.has(schema.tag)) throw new Error(`Element with tag "${schema.tag}" is already registered.`);
23
27
  this.elements.set(schema.tag, schema);
28
+ registerElementBlock(schema);
24
29
  return this;
25
30
  }
26
31
  /**
@@ -0,0 +1,52 @@
1
+ import alert_triangle_fill_svg_default from "../_virtual/_glob-assets_raw/alert-triangle-fill.svg.4cfc4305.js";
2
+ import arrows_sort_svg_default from "../_virtual/_glob-assets_raw/arrows-sort.svg.dda43a85.js";
3
+ import code_svg_default from "../_virtual/_glob-assets_raw/code.svg.c69bc144.js";
4
+ import download_svg_default from "../_virtual/_glob-assets_raw/download.svg.bce82177.js";
5
+ import external_link_svg_default from "../_virtual/_glob-assets_raw/external-link.svg.a9f7f02d.js";
6
+ import file_code_svg_default from "../_virtual/_glob-assets_raw/file-code.svg.0e706f93.js";
7
+ import file_music_svg_default from "../_virtual/_glob-assets_raw/file-music.svg.b9970203.js";
8
+ import file_spreadsheet_svg_default from "../_virtual/_glob-assets_raw/file-spreadsheet.svg.8c03bdfa.js";
9
+ import file_text_svg_default from "../_virtual/_glob-assets_raw/file-text.svg.85bdd923.js";
10
+ import file_type_pdf_svg_default from "../_virtual/_glob-assets_raw/file-type-pdf.svg.bc12db6d.js";
11
+ import file_zip_svg_default from "../_virtual/_glob-assets_raw/file-zip.svg.76dde660.js";
12
+ import file_svg_default from "../_virtual/_glob-assets_raw/file.svg.28f5d6f1.js";
13
+ import photo_svg_default from "../_virtual/_glob-assets_raw/photo.svg.cc273cc1.js";
14
+ import player_stop_fill_svg_default from "../_virtual/_glob-assets_raw/player-stop-fill.svg.5378b752.js";
15
+ import send_svg_default from "../_virtual/_glob-assets_raw/send.svg.02326253.js";
16
+ import sort_ascending_letters_svg_default from "../_virtual/_glob-assets_raw/sort-ascending-letters.svg.14a3df67.js";
17
+ import sort_descending_letters_svg_default from "../_virtual/_glob-assets_raw/sort-descending-letters.svg.3e03647c.js";
18
+ import video_svg_default from "../_virtual/_glob-assets_raw/video.svg.c183f03c.js";
19
+ import world_svg_default from "../_virtual/_glob-assets_raw/world.svg.cd9903c8.js";
20
+ import { IconRegistry } from "@iyulab/components/dist/utilities/icons.js";
21
+ //#region src/utilities/icons.ts
22
+ /**
23
+ * vite의 `import.meta.glob`을 사용하여 chat-components 전용 SVG 아이콘을 빌드 시점에 번들링합니다.
24
+ * 네트워크 요청 없이 즉시 조회 가능하며, 런타임 동안 불변입니다.
25
+ */
26
+ var InternalChatIconBundle = new Map(Object.entries(/* #__PURE__ */ Object.assign({
27
+ "../assets/icons/alert-triangle-fill.svg": alert_triangle_fill_svg_default,
28
+ "../assets/icons/arrows-sort.svg": arrows_sort_svg_default,
29
+ "../assets/icons/code.svg": code_svg_default,
30
+ "../assets/icons/download.svg": download_svg_default,
31
+ "../assets/icons/external-link.svg": external_link_svg_default,
32
+ "../assets/icons/file-code.svg": file_code_svg_default,
33
+ "../assets/icons/file-music.svg": file_music_svg_default,
34
+ "../assets/icons/file-spreadsheet.svg": file_spreadsheet_svg_default,
35
+ "../assets/icons/file-text.svg": file_text_svg_default,
36
+ "../assets/icons/file-type-pdf.svg": file_type_pdf_svg_default,
37
+ "../assets/icons/file-zip.svg": file_zip_svg_default,
38
+ "../assets/icons/file.svg": file_svg_default,
39
+ "../assets/icons/photo.svg": photo_svg_default,
40
+ "../assets/icons/player-stop-fill.svg": player_stop_fill_svg_default,
41
+ "../assets/icons/send.svg": send_svg_default,
42
+ "../assets/icons/sort-ascending-letters.svg": sort_ascending_letters_svg_default,
43
+ "../assets/icons/sort-descending-letters.svg": sort_descending_letters_svg_default,
44
+ "../assets/icons/video.svg": video_svg_default,
45
+ "../assets/icons/world.svg": world_svg_default
46
+ })).map(([path, module]) => {
47
+ return [path.split("/").pop()?.replace(".svg", "") || "", module];
48
+ }).filter(([name]) => name !== ""));
49
+ IconRegistry.register("internal-chat", (name) => {
50
+ return InternalChatIconBundle.get(name);
51
+ });
52
+ //#endregion
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 모델 출력의 이미지가 불러올 수 있는 URL 접두를 정합니다. 렌더하기 전에 한 번 부르세요 —
3
+ * 이미 그려진 블록은 다음 렌더 때 따릅니다.
4
+ *
5
+ * - `undefined`: 전부 허용(기본값).
6
+ * - `[]`: 전부 차단.
7
+ * - `['*']`: 전부 허용(명시).
8
+ * - `['https://cdn.example.com/images/']`: 이 접두로 시작하는 것만.
9
+ *
10
+ * 접두는 **해석된 절대 URL** 과 비교합니다 — 상대 경로는 `document.baseURI` 기준으로 풀린 뒤
11
+ * 비교되므로, 같은 출처 이미지를 허용하려면 `location.origin + '/'` 를 넣으세요.
12
+ * ⚠도메인 전체(`https://example.com/`)보다 경로(`https://example.com/images/`)를 주세요 —
13
+ * 허용한 도메인에 열린 리다이렉트가 있으면 그것을 거쳐 다른 곳으로 나갈 수 있습니다.
14
+ *
15
+ * @example
16
+ * setAllowedImagePrefixes([location.origin + '/', 'https://cdn.example.com/images/']);
17
+ */
18
+ export declare function setAllowedImagePrefixes(prefixes?: readonly string[]): void;
19
+ /** 현재 이미지 출처 정책을 돌려줍니다(`undefined` = 전부 허용). */
20
+ export declare function getAllowedImagePrefixes(): readonly string[] | undefined;
21
+ /**
22
+ * 모델이 쓴 이미지 URL 을 정책에 대어 봅니다. 불러와도 되면 프로토콜 검사를 거친 URL 을,
23
+ * 아니면 `null` 을 돌려줍니다(요청하지 말 것).
24
+ */
25
+ export declare function resolveImageSource(src: string): string | null;
@@ -0,0 +1,61 @@
1
+ import { sanitizeHref } from "./sanitizers.js";
2
+ //#region src/utilities/imageSources.ts
3
+ /**
4
+ * 모델 출력이 불러오는 이미지의 출처 정책 — 페이지 전체에 하나다(CSP `img-src` 와 같은 범위).
5
+ *
6
+ * 마크다운 이미지(`u-marked-block`)와 `u-images-block` 은 모델이 쓴 URL 을 그대로 요청한다.
7
+ * 프롬프트 인젝션된 응답은 이미지 URL 의 쿼리에 데이터를 실어 제3자에게 보낼 수 있다 —
8
+ * 요청은 렌더되는 순간 일어나므로 사용자가 아무것도 누르지 않아도 된다.
9
+ *
10
+ * 기본값은 `undefined`(전부 허용 — 종전 동작)다. 목록을 주면 절대 URL 이 그 접두 중 하나로
11
+ * 시작하는 이미지만 불러오고, 나머지는 요청 없이 «차단된 이미지» 로 그린다.
12
+ * `data:image/…` 는 요청이 없으므로 정책과 무관하게 그린다.
13
+ */
14
+ var allowedPrefixes;
15
+ /** 요청 없이 그려지는 인라인 이미지 */
16
+ var DATA_IMAGE_REGEX = /^data:image\/[a-z0-9.+-]+[;,]/i;
17
+ /**
18
+ * 모델 출력의 이미지가 불러올 수 있는 URL 접두를 정합니다. 렌더하기 전에 한 번 부르세요 —
19
+ * 이미 그려진 블록은 다음 렌더 때 따릅니다.
20
+ *
21
+ * - `undefined`: 전부 허용(기본값).
22
+ * - `[]`: 전부 차단.
23
+ * - `['*']`: 전부 허용(명시).
24
+ * - `['https://cdn.example.com/images/']`: 이 접두로 시작하는 것만.
25
+ *
26
+ * 접두는 **해석된 절대 URL** 과 비교합니다 — 상대 경로는 `document.baseURI` 기준으로 풀린 뒤
27
+ * 비교되므로, 같은 출처 이미지를 허용하려면 `location.origin + '/'` 를 넣으세요.
28
+ * ⚠도메인 전체(`https://example.com/`)보다 경로(`https://example.com/images/`)를 주세요 —
29
+ * 허용한 도메인에 열린 리다이렉트가 있으면 그것을 거쳐 다른 곳으로 나갈 수 있습니다.
30
+ *
31
+ * @example
32
+ * setAllowedImagePrefixes([location.origin + '/', 'https://cdn.example.com/images/']);
33
+ */
34
+ function setAllowedImagePrefixes(prefixes) {
35
+ allowedPrefixes = prefixes ? [...prefixes] : void 0;
36
+ }
37
+ /** 현재 이미지 출처 정책을 돌려줍니다(`undefined` = 전부 허용). */
38
+ function getAllowedImagePrefixes() {
39
+ return allowedPrefixes;
40
+ }
41
+ /**
42
+ * 모델이 쓴 이미지 URL 을 정책에 대어 봅니다. 불러와도 되면 프로토콜 검사를 거친 URL 을,
43
+ * 아니면 `null` 을 돌려줍니다(요청하지 말 것).
44
+ */
45
+ function resolveImageSource(src) {
46
+ const trimmed = src.trim();
47
+ if (DATA_IMAGE_REGEX.test(trimmed)) return trimmed;
48
+ const safe = sanitizeHref(src);
49
+ if (safe === "#") return null;
50
+ if (!allowedPrefixes) return safe;
51
+ if (allowedPrefixes.includes("*")) return safe;
52
+ let absolute;
53
+ try {
54
+ absolute = new URL(safe, document.baseURI).href;
55
+ } catch {
56
+ return null;
57
+ }
58
+ return allowedPrefixes.some((prefix) => absolute.startsWith(prefix)) ? safe : null;
59
+ }
60
+ //#endregion
61
+ export { getAllowedImagePrefixes, resolveImageSource, setAllowedImagePrefixes };
@@ -5,5 +5,5 @@
5
5
  * 이 리포가 채택한 표준이 *"영어 기본 **+ 레지스트리**"* 이기 때문이다 — 영어 리터럴만으로는
6
6
  * 한국어 앱이 그 문구를 되돌릴 방법이 없다. 다음 문자열은 여기로 온다.
7
7
  */
8
- export type ChatMessageKey = 'canvasUnavailable' | 'previousReference' | 'nextReference' | 'closePreview' | 'closeLightbox' | 'previousImage' | 'nextImage' | 'sendMessage' | 'stopGenerating' | 'fullScreen' | 'remove' | 'pngDownload' | 'jsonDownload' | 'search' | 'rowCount' | 'excelDownload' | 'csvDownload' | 'openImage' | 'previewFile';
8
+ export type ChatMessageKey = 'canvasUnavailable' | 'previousReference' | 'nextReference' | 'closePreview' | 'closeLightbox' | 'previousImage' | 'nextImage' | 'sendMessage' | 'stopGenerating' | 'fullScreen' | 'remove' | 'pngDownload' | 'jsonDownload' | 'search' | 'rowCount' | 'excelDownload' | 'csvDownload' | 'openImage' | 'previewFile' | 'imageBlocked' | 'imageBlockedNamed';
9
9
  export declare const messages: import('@iyulab/components/dist/utilities/Locale.js').LocaleNamespace<ChatMessageKey>;
@@ -20,7 +20,9 @@ messages.register("en", {
20
20
  excelDownload: "Excel Download",
21
21
  csvDownload: "CSV Download",
22
22
  openImage: "Open image {index}",
23
- previewFile: "Preview {name}"
23
+ previewFile: "Preview {name}",
24
+ imageBlocked: "Image blocked",
25
+ imageBlockedNamed: "Image blocked: {name}"
24
26
  });
25
27
  messages.register("ko", {
26
28
  canvasUnavailable: "Canvas 2D context를 가져올 수 없습니다.",
@@ -41,7 +43,9 @@ messages.register("ko", {
41
43
  excelDownload: "Excel 다운로드",
42
44
  csvDownload: "CSV 다운로드",
43
45
  openImage: "이미지 {index} 열기",
44
- previewFile: "{name} 미리보기"
46
+ previewFile: "{name} 미리보기",
47
+ imageBlocked: "차단된 이미지",
48
+ imageBlockedNamed: "차단된 이미지: {name}"
45
49
  });
46
50
  //#endregion
47
51
  export { messages };
@@ -23,7 +23,16 @@ export declare function escapeHtmlText(value: string): string;
23
23
  */
24
24
  export declare function escapeHtmlAttr(value: string): string;
25
25
  /**
26
- * href / src attribute용 문자열 처리
26
+ * URL 의 프로토콜을 검사해 안전한 URL 문자열을 돌려줍니다 — HTML escape 는 하지 않습니다.
27
+ * Lit 의 속성 바인딩처럼 escape 를 스스로 하는 자리에서 씁니다(두 번 escape 하면 `&` 가 `&amp;amp;` 가 된다).
28
+ * 위험한 URL(javascript:/data:/vbscript:, protocol-relative)은 `#` 가 됩니다.
29
+ *
30
+ * 예:
31
+ * html`<a href=${sanitizeHref(url)}>`
32
+ */
33
+ export declare function sanitizeHref(value: string): string;
34
+ /**
35
+ * href / src attribute용 문자열 처리 — HTML 문자열을 직접 조립하는 자리에서 씁니다.
27
36
  *
28
37
  * 예:
29
38
  * <a href="{value}">