@iyulab/chat-components 0.12.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/_glob-assets_raw/alert-triangle-fill.svg.4cfc4305.js +4 -0
- package/dist/_virtual/_glob-assets_raw/arrows-sort.svg.dda43a85.js +4 -0
- package/dist/_virtual/_glob-assets_raw/code.svg.c69bc144.js +4 -0
- package/dist/_virtual/_glob-assets_raw/download.svg.bce82177.js +4 -0
- package/dist/_virtual/_glob-assets_raw/external-link.svg.a9f7f02d.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-code.svg.0e706f93.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-music.svg.b9970203.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-spreadsheet.svg.8c03bdfa.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-text.svg.85bdd923.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-type-pdf.svg.bc12db6d.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file-zip.svg.76dde660.js +4 -0
- package/dist/_virtual/_glob-assets_raw/file.svg.28f5d6f1.js +4 -0
- package/dist/_virtual/_glob-assets_raw/photo.svg.cc273cc1.js +4 -0
- package/dist/_virtual/_glob-assets_raw/player-stop-fill.svg.5378b752.js +4 -0
- package/dist/_virtual/_glob-assets_raw/send.svg.02326253.js +4 -0
- package/dist/_virtual/_glob-assets_raw/sort-ascending-letters.svg.14a3df67.js +4 -0
- package/dist/_virtual/_glob-assets_raw/sort-descending-letters.svg.3e03647c.js +4 -0
- package/dist/_virtual/_glob-assets_raw/video.svg.c183f03c.js +4 -0
- package/dist/_virtual/_glob-assets_raw/world.svg.cd9903c8.js +4 -0
- package/dist/components/UDataElement.d.ts +2 -0
- package/dist/components/UDataElement.js +9 -1
- package/dist/components/blocks/UCodeBlock.js +3 -2
- package/dist/components/blocks/UElementBlock.d.ts +12 -16
- package/dist/components/blocks/UElementBlock.js +25 -33
- package/dist/components/blocks/UFileBlock.d.ts +3 -2
- package/dist/components/blocks/UFileBlock.js +12 -10
- package/dist/components/blocks/UFileBlock.styles.js +1 -1
- package/dist/components/blocks/UMarkedBlock.d.ts +8 -1
- package/dist/components/blocks/UMarkedBlock.js +25 -7
- package/dist/components/blocks/UMarkedBlock.styles.js +10 -0
- package/dist/components/blocks/URefBlock.js +2 -2
- package/dist/components/blocks/UTableBlock.js +3 -2
- package/dist/components/blocks/UTextBlock.js +2 -2
- package/dist/components/message/UMessage.js +2 -2
- package/dist/components/prompt/UPrompt.js +5 -3
- package/dist/components/references/URefCard.d.ts +21 -2
- package/dist/components/references/URefCard.js +33 -19
- package/dist/components/references/URefCardGroup.js +2 -2
- package/dist/components/references/URefTag.js +5 -3
- package/dist/components-extra/UChartBlock.js +6 -2
- package/dist/components-extra/UImagesBlock.d.ts +6 -1
- package/dist/components-extra/UImagesBlock.js +30 -7
- package/dist/components-extra/UImagesBlock.styles.js +20 -1
- package/dist/components-extra/UMapBlock.js +5 -2
- package/dist/components-extra/UVideoBlock.js +5 -2
- package/dist/extra.d.ts +7 -1
- package/dist/extra.js +12 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -3
- package/dist/node_modules/katex/dist/katex.js +2 -2
- package/dist/utilities/ElementRegistry.d.ts +13 -0
- package/dist/utilities/ElementRegistry.js +33 -0
- package/dist/utilities/PromptBuilder.d.ts +3 -0
- package/dist/utilities/PromptBuilder.js +5 -0
- package/dist/utilities/icons.js +52 -0
- package/dist/utilities/imageSources.d.ts +25 -0
- package/dist/utilities/imageSources.js +61 -0
- package/dist/utilities/messages.d.ts +1 -1
- package/dist/utilities/messages.js +6 -2
- package/dist/utilities/sanitizers.d.ts +10 -1
- package/dist/utilities/sanitizers.js +16 -5
- package/package.json +4 -2
- package/skills/iyulab-chat-components/SKILL.md +1 -0
- package/skills/iyulab-chat-components/references/components/element-block.md +5 -6
- package/skills/iyulab-chat-components/references/components/images-block.md +1 -0
- package/skills/iyulab-chat-components/references/components/marked-block.md +12 -0
- package/skills/iyulab-chat-components/references/components/ref-card.md +20 -1
- package/skills/iyulab-chat-components/references/extra-system.md +4 -4
- package/skills/iyulab-chat-components/references/security.md +23 -0
|
@@ -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,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
|
-
*
|
|
26
|
+
* URL 의 프로토콜을 검사해 안전한 URL 문자열을 돌려줍니다 — HTML escape 는 하지 않습니다.
|
|
27
|
+
* Lit 의 속성 바인딩처럼 escape 를 스스로 하는 자리에서 씁니다(두 번 escape 하면 `&` 가 `&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}">
|
|
@@ -76,18 +76,29 @@ var HREF_STRIP_CHARS_REGEX = /[\u0000-\u001F\u007F\s]+/g;
|
|
|
76
76
|
/** href/src에서 차단해야 하는 위험 protocol */
|
|
77
77
|
var HREF_UNSAFE_PROTOCOL_REGEX = /^(?:javascript|data|vbscript):/i;
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* URL 의 프로토콜을 검사해 안전한 URL 문자열을 돌려줍니다 — HTML escape 는 하지 않습니다.
|
|
80
|
+
* Lit 의 속성 바인딩처럼 escape 를 스스로 하는 자리에서 씁니다(두 번 escape 하면 `&` 가 `&amp;` 가 된다).
|
|
81
|
+
* 위험한 URL(javascript:/data:/vbscript:, protocol-relative)은 `#` 가 됩니다.
|
|
80
82
|
*
|
|
81
83
|
* 예:
|
|
82
|
-
*
|
|
84
|
+
* html`<a href=${sanitizeHref(url)}>`
|
|
83
85
|
*/
|
|
84
|
-
function
|
|
86
|
+
function sanitizeHref(value) {
|
|
85
87
|
let normalized = value.replace(ZERO_WIDTH_STRICT_REGEX, "").trim();
|
|
86
88
|
if (HREF_STRIP_CHARS_REGEX.test(normalized)) normalized = normalized.replace(HREF_STRIP_CHARS_REGEX, "");
|
|
87
89
|
normalized = normalized.replace(/\\/g, "/");
|
|
88
90
|
if (normalized.startsWith("//")) return "#";
|
|
89
91
|
if (HREF_UNSAFE_PROTOCOL_REGEX.test(normalized)) return "#";
|
|
90
|
-
return
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* href / src attribute용 문자열 처리 — HTML 문자열을 직접 조립하는 자리에서 씁니다.
|
|
96
|
+
*
|
|
97
|
+
* 예:
|
|
98
|
+
* <a href="{value}">
|
|
99
|
+
*/
|
|
100
|
+
function escapeHtmlHref(value) {
|
|
101
|
+
return escapeHtmlAttr(sanitizeHref(value));
|
|
91
102
|
}
|
|
92
103
|
//#endregion
|
|
93
|
-
export { escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, stripZeroWidth };
|
|
104
|
+
export { escapeHtmlAttr, escapeHtmlHref, escapeHtmlText, sanitizeHref, stripZeroWidth };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/chat-components",
|
|
3
3
|
"description": "llm chat components for building chat interfaces",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"author": "iyulab",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"./src/extra.ts",
|
|
39
39
|
"./dist/index.js",
|
|
40
40
|
"./src/index.ts",
|
|
41
|
+
"./dist/utilities/icons.js",
|
|
42
|
+
"./src/utilities/icons.ts",
|
|
41
43
|
"./dist/react/index.js"
|
|
42
44
|
],
|
|
43
45
|
"exports": {
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
"marked": "^18.0.6"
|
|
68
70
|
},
|
|
69
71
|
"peerDependencies": {
|
|
70
|
-
"@iyulab/components": ">=1.
|
|
72
|
+
"@iyulab/components": ">=1.51.0",
|
|
71
73
|
"@lit/react": ">=1.0.8",
|
|
72
74
|
"chart.js": ">=4.5.1",
|
|
73
75
|
"react": ">=18.0.0"
|
|
@@ -31,6 +31,7 @@ import '@iyulab/chat-components/dist/components/prompt/UPrompt.js';
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
> For detailed setup and usage patterns, see [./references/usage.md](./references/usage.md).
|
|
34
|
+
> Model output is untrusted — see [./references/security.md](./references/security.md) for what each input may contain.
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
@@ -6,7 +6,7 @@ import '@iyulab/chat-components/dist/components/blocks/UElementBlock.js';
|
|
|
6
6
|
|
|
7
7
|
**Tag:** `u-element-block`
|
|
8
8
|
|
|
9
|
-
Dynamic
|
|
9
|
+
Dynamic block renderer. Creates the registered block specified by `tag`, binds the schema's `properties` to it, and shows a skeleton placeholder while `loading`. Used internally by `u-marked-block` to render `block-json` code fences (see [../extra-system.md](../extra-system.md)) — you generally don't create it directly.
|
|
10
10
|
|
|
11
11
|
Extends `UDataElement`, so `tag`/`properties` can also be injected via a `<script type="application/json">` slot.
|
|
12
12
|
|
|
@@ -43,15 +43,14 @@ This is converted to `<u-element-block tag="u-images-block" ...>` via `UElementB
|
|
|
43
43
|
|
|
44
44
|
| Property | Type | Default | Reflect | Description |
|
|
45
45
|
|----------|------|---------|---------|-------------|
|
|
46
|
-
| `tag` | `string` | `undefined` | — |
|
|
47
|
-
| `properties` | `Record<string, unknown>` | `undefined` | — |
|
|
46
|
+
| `tag` | `string` | `undefined` | — | Block tag to render. Must be a registered block (`registerElementBlock` or `ElementPromptBuilder.add`) whose element is defined |
|
|
47
|
+
| `properties` | `Record<string, unknown>` | `undefined` | — | Values to bind. Only names listed in the block schema's `properties` are assigned |
|
|
48
48
|
| `loading` | `boolean` | `false` | ✓ | Shows a skeleton placeholder instead of the element |
|
|
49
|
-
| `blacklist` | `string[]` | `['innerHTML', 'outerHTML', 'textContent', 'innerText', 'outerText', 'srcdoc']` | — | Property names blocked from binding for XSS prevention |
|
|
50
49
|
|
|
51
50
|
## Error Handling
|
|
52
51
|
|
|
53
|
-
If `tag` is missing
|
|
52
|
+
If `tag` is missing or not a registered block, or `properties` is not an object or fails to assign, `u-element-block` does **not** show an error card — it renders nothing and logs to the console (`[u-element-block] ...`). While `loading` is `true`, JSON parse failures from the underlying `UDataElement` are silently ignored too, since incomplete streamed JSON is expected mid-stream.
|
|
54
53
|
|
|
55
54
|
## Security
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
`tag` and `properties` usually come from model output, so they are treated as untrusted. Only registered blocks are created, and only the property names in the block's schema are assigned — any other key (for example `style`) is dropped and logged. See [../extra-system.md](../extra-system.md#security).
|
|
@@ -71,3 +71,4 @@ interface ImageSlide {
|
|
|
71
71
|
| Carousel | Up to 3 slides visible; draggable |
|
|
72
72
|
| Lightbox | Full-screen modal on image click |
|
|
73
73
|
| Keyboard | `←` `→` to navigate, `Esc` to close |
|
|
74
|
+
| Image origin policy | `setAllowedImagePrefixes()` applies — a blocked item shows its alt text and is not requested (see [../security.md](../security.md)) |
|
|
@@ -65,6 +65,18 @@ Vendored from GitHub's markdown theme — light values shown, dark auto-applies
|
|
|
65
65
|
| `--base-size-4` / `-8` / `-16` / `-24` / `-40` | Spacing scale used throughout (margins, padding, gaps) |
|
|
66
66
|
| `--base-text-weight-normal` / `-medium` / `-semibold` | Font-weight scale (body / table header / heading) |
|
|
67
67
|
|
|
68
|
+
## CSS Parts
|
|
69
|
+
|
|
70
|
+
| Part | Description |
|
|
71
|
+
|------|-------------|
|
|
72
|
+
| `image-blocked` | Alt text shown in place of an image the image origin policy blocked (see Images) |
|
|
73
|
+
|
|
74
|
+
## Images
|
|
75
|
+
|
|
76
|
+
Markdown images (table cells included) follow the page-wide image origin policy set with
|
|
77
|
+
`setAllowedImagePrefixes()`. A blocked image is not requested; its alt text is shown in a
|
|
78
|
+
`part="image-blocked"` span (`Image blocked: …`, localized). See [../security.md](../security.md).
|
|
79
|
+
|
|
68
80
|
## ReferenceCitation Type
|
|
69
81
|
|
|
70
82
|
```ts
|
|
@@ -6,7 +6,7 @@ import '@iyulab/chat-components/dist/components/references/URefCard.js';
|
|
|
6
6
|
|
|
7
7
|
**Tag:** `u-ref-card`
|
|
8
8
|
|
|
9
|
-
Displays a single reference source as a card. Supports `web` and `document` types.
|
|
9
|
+
Displays a single reference source as a card. Supports `web` and `document` types. A web card shows a favicon only when you give it a resolver — by default it makes no request of its own (see [Favicon](#favicon)).
|
|
10
10
|
|
|
11
11
|
Extends `UDataElement`, so data can also be injected via a `<script type="application/json">` slot.
|
|
12
12
|
|
|
@@ -46,3 +46,22 @@ Extends `UDataElement`, so data can also be injected via a `<script type="applic
|
|
|
46
46
|
| `title` | `string` | `''` | — | Card title (falls back to domain name for web type) |
|
|
47
47
|
| `snippet` | `string` | `undefined` | — | Excerpt text |
|
|
48
48
|
| `tags` | `string[]` | `undefined` | — | Tag list. Pass as JSON array string via HTML attribute |
|
|
49
|
+
| `faviconUrl` | `FaviconResolver` | `undefined` | — | Favicon resolver for this card (property only). Overrides `URefCard.defaultFaviconUrl` |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Favicon
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { URefCard, googleFaviconUrl } from '@iyulab/chat-components';
|
|
57
|
+
|
|
58
|
+
// type FaviconResolver = (url: string) => string | undefined
|
|
59
|
+
URefCard.defaultFaviconUrl = (url) => `/favicons/${new URL(url).hostname}.png`; // your own host
|
|
60
|
+
// or, where the public internet is reachable and sending host names to Google is acceptable:
|
|
61
|
+
URefCard.defaultFaviconUrl = googleFaviconUrl;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **Default: no favicon, no request.** The card never contacts a third party unless you choose one — safe on a closed network.
|
|
65
|
+
- `URefCard.defaultFaviconUrl` applies to every card, including those rendered inside `u-ref-block`, `u-ref-card-group` and marked-block tooltips. Set it before cards render.
|
|
66
|
+
- A card's own `faviconUrl` property wins over the module-wide one. Returning `undefined` draws the card without a favicon.
|
|
67
|
+
- `googleFaviconUrl` sends each card's host name (not the full URL) to `www.google.com`.
|
|
@@ -76,20 +76,20 @@ const def: ElementSchema = {
|
|
|
76
76
|
ElementPromptBuilder.instance.add(def);
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
The element just needs to accept standard property assignments — `u-element-block`
|
|
79
|
+
The element just needs to accept standard property assignments. `add()` also registers the schema as a renderable block — `u-element-block` assigns only the names in the schema's `properties`. If you write your own prompt instead, call `registerElementBlock(def)` (exported from `@iyulab/chat-components`).
|
|
80
80
|
|
|
81
81
|
## Error Handling
|
|
82
82
|
|
|
83
|
-
If the LLM outputs a `tag` that isn't registered (e.g. missing `@iyulab/chat-components/extra` import, or a custom extra's element isn't defined yet), `u-element-block` does **not** show an error card — it renders nothing and logs to the console instead. Genuine data errors (invalid `properties`, assignment failures) are also console-only.
|
|
83
|
+
If the LLM outputs a `tag` that isn't a registered block (e.g. missing `@iyulab/chat-components/extra` import, or a custom extra's schema was never registered or its element isn't defined yet), `u-element-block` does **not** show an error card — it renders nothing and logs to the console instead. Genuine data errors (invalid `properties`, assignment failures) are also console-only.
|
|
84
84
|
|
|
85
85
|
## Security
|
|
86
86
|
|
|
87
|
-
`u-element-block`
|
|
87
|
+
`block-json` is model output and is treated as untrusted. `u-element-block` creates only registered blocks (`ElementPromptBuilder.add()` or `registerElementBlock()`; built-in extras register on import) and assigns only the property names in the block's schema `properties` — other keys, including `style`, are dropped and logged. Custom extras should accept data only; if a property is rendered as HTML, sanitize it inside the element.
|
|
88
88
|
|
|
89
89
|
## `ElementPromptBuilder` API
|
|
90
90
|
|
|
91
91
|
| Method | Description |
|
|
92
92
|
|--------|-------------|
|
|
93
93
|
| `ElementPromptBuilder.instance` | Singleton accessor |
|
|
94
|
-
| `.add(definition: ElementSchema)` | Register a custom extra (throws on tag conflict) |
|
|
94
|
+
| `.add(definition: ElementSchema)` | Register a custom extra (throws on tag conflict). Also registers it as a renderable block |
|
|
95
95
|
| `.build(): string` | Generate system prompt instruction string |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
Model output is **untrusted** (prompt injection can put third-party text in any response). What each input
|
|
4
|
+
is trusted to contain:
|
|
5
|
+
|
|
6
|
+
| Input | Treated as | Handling |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `u-marked-block` `value` | untrusted | Raw HTML shown as text · link/image URLs protocol-checked (`javascript:`/`data:`/`vbscript:`/`//host` → `#`) · code fences escaped · table cells same rules |
|
|
9
|
+
| `block-json` fences | untrusted | Only registered blocks; only the schema's property names are assigned |
|
|
10
|
+
| `refs` | untrusted | Text rendered as text · URLs protocol-checked in `u-ref-tag` and `u-ref-card` |
|
|
11
|
+
| `u-table-block` cell `text` bound directly | **trusted HTML** | Sanitize before binding. `block-json` cannot reach it |
|
|
12
|
+
| Your custom block | your element's contract | The schema allows names, not content — sanitize HTML-rendered properties inside the element |
|
|
13
|
+
|
|
14
|
+
Requests: markdown images and `u-images-block` load the `http:`/`https:` URL the model writes as soon as they
|
|
15
|
+
render (an image URL can carry data out). When the model reads content you do not control, call
|
|
16
|
+
`setAllowedImagePrefixes([location.origin + '/', 'https://cdn.example.com/images/'])` before rendering —
|
|
17
|
+
images outside the list are not requested and show their alt text (`Image blocked: …`). Prefixes match the
|
|
18
|
+
resolved absolute URL; prefer paths to whole domains (open redirects). `data:image/…` is always shown (no
|
|
19
|
+
request). Default: all allowed. Add CSP `img-src` as a second layer.
|
|
20
|
+
`u-map-block` (OpenStreetMap iframe) and `u-video-block` (YouTube/Vimeo/direct) embed external content.
|
|
21
|
+
`u-ref-card` requests no favicon by default.
|
|
22
|
+
|
|
23
|
+
Do not insert model output with `innerHTML` yourself; route it through `u-marked-block`.
|