@braincrew-lab/langchain-canvas 0.1.14 → 0.3.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/README.ko.md +23 -11
- package/README.md +23 -11
- package/dist/{ChartRenderer-FX6ZN4NC.js → ChartRenderer-ABRQ5YFN.js} +67 -39
- package/dist/{DocumentRenderer-KJ6FTGKH.js → DocumentRenderer-YTEMC3Z4.js} +17 -12
- package/dist/PdfRenderer-GDA67MES.js +98 -0
- package/dist/SlidesRenderer-JNPXNTNJ.js +964 -0
- package/dist/TableRenderer-BJQE2HMO.js +636 -0
- package/dist/{chunk-TERWLUW3.js → chunk-FSFOURG5.js} +1 -1
- package/dist/chunk-QMOJEGRH.js +207 -0
- package/dist/{chunk-KKLWKR5G.js → chunk-ZLXAWRUP.js} +52 -20
- package/dist/index.d.ts +779 -6
- package/dist/index.js +2623 -183
- package/dist/styles.css +477 -4
- package/package.json +26 -2
- package/dist/SlidesRenderer-SK5VQDIL.js +0 -492
- package/dist/TableRenderer-Y4JFRSLU.js +0 -301
package/README.ko.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @braincrew-lab/langchain-canvas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@braincrew-lab/langchain-canvas)
|
|
4
|
+
[](https://github.com/braincrew-lab/langchain-canvas/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
LangChain 챗 앱을 위한 캔버스. 에이전트가 아티팩트(웹 페이지·스프레드시트·슬라이드·차트·문서·PDF)를 스트리밍하면 대화 옆에 실시간으로 뜨고 그 자리에서 편집됩니다. 사용자가 직접 손보고 실제 파일로 내보낼 수 있습니다.
|
|
7
|
+
|
|
8
|
+
ChatGPT Canvas나 Claude Artifacts 같은 경험을, **내 React 앱에 넣고 내 에이전트에 연결하는 패키지**로 제공합니다. 스크린샷·아키텍처·와이어 프로토콜 전체는 [GitHub 레포](https://github.com/braincrew-lab/langchain-canvas)에 있습니다.
|
|
6
9
|
|
|
7
10
|
> [English README](./README.md)
|
|
8
11
|
|
|
@@ -74,26 +77,29 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
|
|
|
74
77
|
|
|
75
78
|
정적 표가 아니라 스프레드시트 엔진(Fortune-sheet) 위에서 동작합니다.
|
|
76
79
|
|
|
77
|
-
- **실시간 수식** — `=SUM(C2:C4)`, `=
|
|
80
|
+
- **실시간 수식** — `=SUM(C2:C4)`, `=VLOOKUP(...)`, `=A2*B2` 입력 시 계산. 셀참조·범위·함수 자동완성 지원. 계산은 선택 설치 `fast-formula-parser`가 담당해 Excel 함수 세트(`IF`, `COUNTIF`, `SUMIF`, `INDEX`/`MATCH`, …)가 바로 동작
|
|
78
81
|
- **데이터로 온 수식** — 에이전트가 값으로 보낸 수식(`"=AVERAGE(B2:B4)"`)은 **로드 시 미리 계산**되어 결과가 바로 표시
|
|
79
|
-
-
|
|
82
|
+
- **Excel 스타일 리본** — 삽입 / 글꼴 / 맞춤 / 표시 형식 / 편집 / 보기로 그룹된 트레이, **수식 바**, **Σ 자동 합계** 메뉴
|
|
83
|
+
- **서식** — 폰트, 숫자/통화/%/날짜 서식, 굵게/기울임, 테두리, 셀 병합/해제, 정렬, **틀 고정**, 다중 시트, 그리고 에이전트가 만든 요란한 셀 색을 한 번에 걷어내는 **스타일 정리**
|
|
80
84
|
- 넓은 그리드에서 **상하좌우 부드러운 스크롤**
|
|
81
|
-
- **Export** — `.xlsx`(폰트/병합/서식 포함) 또는 `.csv`
|
|
85
|
+
- **Export** — `.xlsx`(폰트/병합/서식 포함) 또는 `.csv` — 둘 다 사용자의 그리드 편집이 반영됨
|
|
82
86
|
|
|
83
87
|
### 🖼️ 슬라이드 (`slides`) — 프리 캔버스 덱
|
|
84
88
|
|
|
85
89
|
모든 요소가 움직이는 PowerPoint 스타일 편집기.
|
|
86
90
|
|
|
87
|
-
- **자유 배치** — 텍스트/이미지 요소 드래그·리사이즈,
|
|
91
|
+
- **자유 배치** — 텍스트/이미지 요소 드래그·리사이즈, 드래그와 리사이즈 모두에서 스냅 가이드, 회전, Shift로 비율 고정 리사이즈
|
|
92
|
+
- **다중 선택 & 그룹** — Shift+클릭 또는 마퀴 선택 후 그룹/해제(⌘G / ⌘⇧G). 떠 있는 액션 바로 정렬·분배·복제·삭제
|
|
93
|
+
- **키보드 편집** — 방향키 미세 이동, ⌘D 복제, ⌘C/⌘V(슬라이드 간 가능), ⌘] / ⌘[ z-순서
|
|
88
94
|
- **인라인 편집** — 더블클릭 텍스트 편집, 굵게/크기/색/정렬 툴바
|
|
89
95
|
- **구조** — 슬라이드 추가/복제/삭제/순서, 썸네일 레일, 발표자 노트
|
|
90
|
-
-
|
|
91
|
-
- **Export** — `.pptx
|
|
96
|
+
- **8가지 테마**(Editorial · Gallery · Boardroom · Sage · Graphite · Observatory · Ultramarine · Bordeaux)·배경, 발표 모드(전체화면, 방향키 이동, 진행 표시)
|
|
97
|
+
- **Export** — `.pptx` 또는 **PDF**(전 슬라이드)
|
|
92
98
|
|
|
93
99
|
### 📝 문서 (`document`) — Markdown / Word
|
|
94
100
|
|
|
95
101
|
- **클릭 편집** — 페이지를 Markdown으로 편집(GFM 렌더)
|
|
96
|
-
- **Export** — `.docx`, `.md`, `.pdf`, `.html`
|
|
102
|
+
- **Export** — `.docx`, `.md`, `.hwpx`(한글), `.pdf`, `.html`
|
|
97
103
|
|
|
98
104
|
### 📈 차트 (`chart`)
|
|
99
105
|
|
|
@@ -102,10 +108,15 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
|
|
|
102
108
|
- 시리즈(또는 파이 조각)별 **색상 변경**, 시리즈 이름, Y축 라벨, 스택 토글
|
|
103
109
|
- **Export** — `.pdf`(차트가 SVG라 선명) 또는 raw JSON
|
|
104
110
|
|
|
111
|
+
### 📄 PDF (`pdf`)
|
|
112
|
+
|
|
113
|
+
- 에이전트가 `{ type: "pdf", data: { src } }`(`data:`/`blob:`/https URL)를 emit 하면 **브라우저 내장 뷰어**로 렌더 — 의존성 0, 줌 컨트롤 포함
|
|
114
|
+
- `data:` 소스는 `application/pdf`로 고정되어, 조작된 아티팩트가 스크립트 실행 가능한 same-origin 프레임을 끼워 넣을 수 없음
|
|
115
|
+
|
|
105
116
|
### 📁 파일 — 라운드트립
|
|
106
117
|
|
|
107
|
-
- **Import** — 드래그앤드롭 또는 파일 선택: **CSV · Excel · Markdown · HTML · JSON** → 편집 가능한
|
|
108
|
-
- **Export** — 각 아티팩트를 네이티브
|
|
118
|
+
- **Import** — 드래그앤드롭 또는 파일 선택: **CSV · Excel(.xlsx) · Word(.docx) · PowerPoint(.pptx) · PDF · Markdown · HTML · JSON · 한글(.hwp / .hwpx)** → 편집 가능한 아티팩트로. `.xlsx`는 폰트/채우기/병합/서식/내장 이미지를, `.pptx`는 테마 색·마스터 스타일을 유지하고, 바이너리 `.hwp`는 직접 파싱해 서식 있는 HTML로 엽니다
|
|
119
|
+
- **Export** — 각 아티팩트를 네이티브 포맷(`.xlsx` / `.csv` / `.docx` / `.md` / `.hwpx` / `.pptx` / 차트 JSON)으로, 그리고 범용 **standalone `.html`** · **PDF**(브라우저 인쇄)
|
|
109
120
|
|
|
110
121
|
### 🧰 모든 아티팩트 공통
|
|
111
122
|
|
|
@@ -121,6 +132,7 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
|
|
|
121
132
|
- **peerDependency:** React 18 또는 19 — 직접 제공. ESM 전용.
|
|
122
133
|
- **스타일:** `import "@braincrew-lab/langchain-canvas/styles.css"` 한 번.
|
|
123
134
|
- **인스턴스 격리:** `<CanvasProvider>`가 서브트리마다 독립 store 제공.
|
|
135
|
+
- **한국어 UI:** `<Canvas locale="ko" />` 하나로 UI 전체가 한국어/영어 전환.
|
|
124
136
|
- **커스텀 렌더러:** `registry`로 타입 렌더 방식 추가/오버라이드.
|
|
125
137
|
|
|
126
138
|
```tsx
|
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @braincrew-lab/langchain-canvas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@braincrew-lab/langchain-canvas)
|
|
4
|
+
[](https://github.com/braincrew-lab/langchain-canvas/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
A canvas for your LangChain chat app. Your agent streams an artifact — a web page, a spreadsheet, a slide deck, a chart, a document, a PDF — and it shows up next to the conversation, live and editable. Users can tweak it by hand and export it to a real file.
|
|
7
|
+
|
|
8
|
+
Think ChatGPT Canvas or Claude Artifacts, but a package you drop into your own React app and point at your own agent. Screenshots, architecture, and the full wire protocol live in the [GitHub repo](https://github.com/braincrew-lab/langchain-canvas).
|
|
6
9
|
|
|
7
10
|
> [한국어 README](./README.ko.md)
|
|
8
11
|
|
|
@@ -74,26 +77,29 @@ HTML is the base substrate, rendered in a sandboxed iframe. Selecting and editin
|
|
|
74
77
|
|
|
75
78
|
Runs on a spreadsheet engine (Fortune-sheet), not a static table.
|
|
76
79
|
|
|
77
|
-
- **Live formulas** — type `=SUM(C2:C4)`, `=
|
|
80
|
+
- **Live formulas** — type `=SUM(C2:C4)`, `=VLOOKUP(...)`, `=A2*B2`; they calculate, with cell references, ranges, and function autocomplete. Evaluation is delegated to the optional `fast-formula-parser`, so the Excel function set (`IF`, `COUNTIF`, `SUMIF`, `INDEX`/`MATCH`, …) works out of the box.
|
|
78
81
|
- **Formulas from data** — a formula the agent sends as a value (e.g. `"=AVERAGE(B2:B4)"`) is **pre-computed on load** so it shows its result immediately.
|
|
79
|
-
- **
|
|
82
|
+
- **Excel-style ribbon** — grouped Insert / Font / Alignment / Number / Editing / View trays, plus a **formula bar** and a **Σ AutoSum** menu.
|
|
83
|
+
- **Formatting** — fonts, number/currency/percent/date formats, bold/italic, borders, cell merge/unmerge, alignment, **freeze panes**, multiple sheets, and a one-click **clean styling** that strips garish agent-generated fills.
|
|
80
84
|
- **Smooth scrolling** in both directions over a large grid.
|
|
81
|
-
- **Export** to `.xlsx` (with fonts/merges/formats) or `.csv
|
|
85
|
+
- **Export** to `.xlsx` (with fonts/merges/formats) or `.csv` — both include the user's in-grid edits.
|
|
82
86
|
|
|
83
87
|
### 🖼️ Slides (`slides`) — a free-canvas deck
|
|
84
88
|
|
|
85
89
|
A PowerPoint-style editor where every element is movable.
|
|
86
90
|
|
|
87
|
-
- **Free positioning** — drag and resize text/image elements; snap to
|
|
91
|
+
- **Free positioning** — drag and resize text/image elements; snap guides on drag *and* resize; rotation; hold Shift to lock aspect while resizing.
|
|
92
|
+
- **Multi-select & groups** — Shift+click or marquee-select, then group/ungroup (⌘G / ⌘⇧G); a floating bar offers align, distribute, duplicate, delete.
|
|
93
|
+
- **Keyboard editing** — arrow-key nudge, ⌘D duplicate, ⌘C/⌘V across slides, ⌘] / ⌘[ z-order.
|
|
88
94
|
- **Inline editing** — double-click to edit text; format toolbar for bold/size/color/align.
|
|
89
95
|
- **Structure** — add/duplicate/delete/reorder slides, thumbnails rail, speaker notes.
|
|
90
|
-
- **
|
|
91
|
-
- **Export** to `.pptx
|
|
96
|
+
- **8 themes** (Editorial, Gallery, Boardroom, Sage, Graphite, Observatory, Ultramarine, Bordeaux) & backgrounds; present mode (full-screen, arrow-key navigation, progress bar).
|
|
97
|
+
- **Export** to `.pptx` or **PDF** (all slides).
|
|
92
98
|
|
|
93
99
|
### 📝 Documents (`document`) — Markdown / Word
|
|
94
100
|
|
|
95
101
|
- **Click-to-edit** the page as Markdown, rendered with GFM.
|
|
96
|
-
- **Export** to `.docx`, `.md`, `.pdf`, or `.html`.
|
|
102
|
+
- **Export** to `.docx`, `.md`, `.hwpx` (한글), `.pdf`, or `.html`.
|
|
97
103
|
|
|
98
104
|
### 📈 Charts (`chart`)
|
|
99
105
|
|
|
@@ -102,10 +108,15 @@ A PowerPoint-style editor where every element is movable.
|
|
|
102
108
|
- **Recolor** each series (or each pie slice), rename series, set the y-axis label, toggle stacking.
|
|
103
109
|
- **Export** to `.pdf` (the chart is SVG, so it prints crisply) or the raw JSON.
|
|
104
110
|
|
|
111
|
+
### 📄 PDFs (`pdf`)
|
|
112
|
+
|
|
113
|
+
- The agent emits `{ type: "pdf", data: { src } }` (a `data:`/`blob:`/https URL) and the file renders in the **browser's built-in viewer** — zero dependencies, with a zoom control.
|
|
114
|
+
- `data:` sources are pinned to `application/pdf`, so a crafted artifact can't smuggle a scriptable same-origin frame.
|
|
115
|
+
|
|
105
116
|
### 📁 Files — round-trip
|
|
106
117
|
|
|
107
|
-
- **Import** by drag-and-drop or a file picker: **CSV · Excel · Markdown · HTML · JSON**. They open as editable artifacts.
|
|
108
|
-
- **Export** every artifact to its native format, plus a universal **standalone `.html`** and **PDF** (browser print).
|
|
118
|
+
- **Import** by drag-and-drop or a file picker: **CSV · Excel (.xlsx) · Word (.docx) · PowerPoint (.pptx) · PDF · Markdown · HTML · JSON · 한글 (.hwp / .hwpx)**. They open as editable artifacts — `.xlsx` keeps fonts/fills/merges/formats/embedded images, `.pptx` keeps theme colors and master styles, and binary `.hwp` is parsed from scratch into formatted HTML.
|
|
119
|
+
- **Export** every artifact to its native format (`.xlsx` / `.csv` / `.docx` / `.md` / `.hwpx` / `.pptx` / chart JSON), plus a universal **standalone `.html`** and **PDF** (browser print).
|
|
109
120
|
|
|
110
121
|
### 🧰 Across every artifact
|
|
111
122
|
|
|
@@ -121,6 +132,7 @@ A PowerPoint-style editor where every element is movable.
|
|
|
121
132
|
- **Peer dependency:** React 18 or 19 — you bring your own. ESM only.
|
|
122
133
|
- **Styles:** `import "@braincrew-lab/langchain-canvas/styles.css"` once.
|
|
123
134
|
- **Isolated instances:** `<CanvasProvider>` gives each subtree its own store.
|
|
135
|
+
- **Korean UI:** `<Canvas locale="ko" />` localizes the entire chrome (English / Korean).
|
|
124
136
|
- **Bring your own renderer:** pass `registry` to add or override how a type renders.
|
|
125
137
|
|
|
126
138
|
```tsx
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { useArtifactPatch } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
import { useArtifactPatch } from './chunk-FSFOURG5.js';
|
|
2
|
+
import './chunk-ZLXAWRUP.js';
|
|
3
|
+
import { useT } from './chunk-QMOJEGRH.js';
|
|
3
4
|
import { useState, useRef, useMemo, useEffect } from 'react';
|
|
4
5
|
import * as echarts from 'echarts/core';
|
|
5
|
-
import { BarChart, LineChart, PieChart } from 'echarts/charts';
|
|
6
|
+
import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
|
|
6
7
|
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
|
|
7
8
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
8
9
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
9
10
|
|
|
10
|
-
echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
|
|
11
|
+
echarts.use([BarChart, LineChart, PieChart, ScatterChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
|
|
11
12
|
var PALETTE = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#0ea5e9", "#a855f7"];
|
|
12
13
|
var seriesColor = (series, index) => series.color ?? PALETTE[index % PALETTE.length];
|
|
13
14
|
var sliceColor = (options, index) => options?.colors?.[index] ?? PALETTE[index % PALETTE.length];
|
|
14
|
-
var CHART_TYPES = ["bar", "line", "area", "pie"];
|
|
15
|
+
var CHART_TYPES = ["bar", "line", "area", "scatter", "pie", "donut"];
|
|
15
16
|
function EChart({ option, height, onInst }) {
|
|
16
17
|
const boxRef = useRef(null);
|
|
17
18
|
const instRef = useRef(null);
|
|
@@ -35,6 +36,7 @@ function EChart({ option, height, onInst }) {
|
|
|
35
36
|
return /* @__PURE__ */ jsx("div", { ref: boxRef, className: "cv-chart__canvas", style: { width: "100%", height } });
|
|
36
37
|
}
|
|
37
38
|
function ChartRenderer({ artifact }) {
|
|
39
|
+
const t = useT();
|
|
38
40
|
const { chart, rows, xKey, series, options, echartsOption } = artifact.data;
|
|
39
41
|
const patch = useArtifactPatch(artifact.id);
|
|
40
42
|
const [editing, setEditing] = useState(false);
|
|
@@ -56,15 +58,15 @@ function ChartRenderer({ artifact }) {
|
|
|
56
58
|
return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
|
|
57
59
|
/* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
|
|
58
60
|
/* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
|
|
59
|
-
/* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: "
|
|
61
|
+
/* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: t("downloadPng"), children: t("downloadPng") })
|
|
60
62
|
] }),
|
|
61
63
|
/* @__PURE__ */ jsx(EChart, { option, height: 320, onInst: (i) => instRef.current = i })
|
|
62
64
|
] });
|
|
63
65
|
}
|
|
64
66
|
if (rows.length === 0) {
|
|
65
|
-
return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: "
|
|
67
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: t("waitingData") });
|
|
66
68
|
}
|
|
67
|
-
const isPie = chart === "pie";
|
|
69
|
+
const isPie = chart === "pie" || chart === "donut";
|
|
68
70
|
const setSeries = (i, partial) => patch({ series: series.map((s, j) => j === i ? { ...s, ...partial } : s) });
|
|
69
71
|
const setSliceColor = (i, color) => {
|
|
70
72
|
const colors = rows.map((_, j) => options?.colors?.[j] ?? PALETTE[j % PALETTE.length]);
|
|
@@ -80,38 +82,38 @@ function ChartRenderer({ artifact }) {
|
|
|
80
82
|
const removeRow = (i) => patch({ rows: rows.filter((_, j) => j !== i) });
|
|
81
83
|
return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
|
|
82
84
|
/* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
|
|
83
|
-
/* @__PURE__ */ jsx("div", { className: "cv-chart__types", children: CHART_TYPES.map((
|
|
85
|
+
/* @__PURE__ */ jsx("div", { className: "cv-chart__types", children: CHART_TYPES.map((t2) => /* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${chart === t2 ? "is-primary" : ""}`, onClick: () => patch({ chart: t2 }), children: t2 }, t2)) }),
|
|
84
86
|
(chart === "bar" || chart === "area") && /* @__PURE__ */ jsxs("label", { className: "cv-chart__stack", children: [
|
|
85
87
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: !!options?.stacked, onChange: (e) => patch({ options: { ...options, stacked: e.target.checked } }) }),
|
|
86
|
-
"
|
|
88
|
+
t("stacked")
|
|
87
89
|
] }),
|
|
88
90
|
/* @__PURE__ */ jsx(
|
|
89
91
|
"input",
|
|
90
92
|
{
|
|
91
93
|
className: "cv-chart__title-input",
|
|
92
94
|
value: options?.title ?? "",
|
|
93
|
-
placeholder: "
|
|
95
|
+
placeholder: t("chartTitle"),
|
|
94
96
|
onChange: (e) => patch({ options: { ...options, title: e.target.value || void 0 } }),
|
|
95
|
-
title: "
|
|
97
|
+
title: t("chartTitle")
|
|
96
98
|
}
|
|
97
99
|
),
|
|
98
100
|
/* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
|
|
99
|
-
/* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: "
|
|
100
|
-
/* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${editing ? "is-primary" : ""}`, onClick: () => setEditing((v) => !v), children: editing ? "
|
|
101
|
+
/* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: t("downloadPng"), children: t("downloadPng") }),
|
|
102
|
+
/* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${editing ? "is-primary" : ""}`, onClick: () => setEditing((v) => !v), children: editing ? t("doneEditing") : t("editData") })
|
|
101
103
|
] }),
|
|
102
104
|
editing && /* @__PURE__ */ jsxs("div", { className: "cv-chart__editor cv-chrome", children: [
|
|
103
105
|
/* @__PURE__ */ jsx("div", { className: "cv-chart__legend", children: isPie ? rows.map((r, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
|
|
104
106
|
/* @__PURE__ */ jsx("input", { type: "color", value: sliceColor(options, i), onChange: (e) => setSliceColor(i, e.target.value), title: `Color: ${r[xKey]}` }),
|
|
105
107
|
/* @__PURE__ */ jsx("span", { className: "cv-chart__swatch-label", children: String(r[xKey]) })
|
|
106
108
|
] }, i)) : series.map((s, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
|
|
107
|
-
/* @__PURE__ */ jsx("input", { type: "color", value: seriesColor(s, i), onChange: (e) => setSeries(i, { color: e.target.value }), title: "
|
|
109
|
+
/* @__PURE__ */ jsx("input", { type: "color", value: seriesColor(s, i), onChange: (e) => setSeries(i, { color: e.target.value }), title: t("seriesColor") }),
|
|
108
110
|
/* @__PURE__ */ jsx(
|
|
109
111
|
"input",
|
|
110
112
|
{
|
|
111
113
|
className: "cv-chart__series-name",
|
|
112
114
|
value: s.label ?? s.key,
|
|
113
115
|
onChange: (e) => setSeries(i, { label: e.target.value }),
|
|
114
|
-
title: "
|
|
116
|
+
title: t("seriesName")
|
|
115
117
|
}
|
|
116
118
|
)
|
|
117
119
|
] }, s.key)) }),
|
|
@@ -124,6 +126,23 @@ function ChartRenderer({ artifact }) {
|
|
|
124
126
|
/* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340, onInst: (i) => instRef.current = i })
|
|
125
127
|
] });
|
|
126
128
|
}
|
|
129
|
+
function NumberCell({ value, onCommit }) {
|
|
130
|
+
const [draft, setDraft] = useState(null);
|
|
131
|
+
const shown = draft ?? String(Number(value ?? 0));
|
|
132
|
+
return /* @__PURE__ */ jsx(
|
|
133
|
+
"input",
|
|
134
|
+
{
|
|
135
|
+
type: "number",
|
|
136
|
+
value: shown,
|
|
137
|
+
onChange: (e) => {
|
|
138
|
+
setDraft(e.target.value);
|
|
139
|
+
const n = Number(e.target.value);
|
|
140
|
+
if (e.target.value !== "" && Number.isFinite(n)) onCommit(n);
|
|
141
|
+
},
|
|
142
|
+
onBlur: () => setDraft(null)
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
}
|
|
127
146
|
function DataGrid({
|
|
128
147
|
rows,
|
|
129
148
|
xKey,
|
|
@@ -132,6 +151,7 @@ function DataGrid({
|
|
|
132
151
|
onAddRow,
|
|
133
152
|
onRemoveRow
|
|
134
153
|
}) {
|
|
154
|
+
const t = useT();
|
|
135
155
|
return /* @__PURE__ */ jsxs("div", { className: "cv-chart__grid", children: [
|
|
136
156
|
/* @__PURE__ */ jsxs("table", { children: [
|
|
137
157
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
@@ -141,25 +161,18 @@ function DataGrid({
|
|
|
141
161
|
] }) }),
|
|
142
162
|
/* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
143
163
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { value: String(row[xKey] ?? ""), onChange: (e) => onCell(i, xKey, e.target.value) }) }),
|
|
144
|
-
series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
type: "number",
|
|
148
|
-
value: Number(row[s.key] ?? 0),
|
|
149
|
-
onChange: (e) => onCell(i, s.key, e.target.value === "" ? 0 : Number(e.target.value))
|
|
150
|
-
}
|
|
151
|
-
) }, s.key)),
|
|
152
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("button", { className: "cv-chart__row-del", onClick: () => onRemoveRow(i), disabled: rows.length <= 1, title: "Remove row", children: "\xD7" }) })
|
|
164
|
+
series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(NumberCell, { value: row[s.key], onCommit: (n) => onCell(i, s.key, n) }) }, s.key)),
|
|
165
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("button", { className: "cv-chart__row-del", onClick: () => onRemoveRow(i), disabled: rows.length <= 1, title: t("removeRow"), children: "\xD7" }) })
|
|
153
166
|
] }, i)) })
|
|
154
167
|
] }),
|
|
155
|
-
/* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: "
|
|
168
|
+
/* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: t("addRowBtn") })
|
|
156
169
|
] });
|
|
157
170
|
}
|
|
158
171
|
function toEChartsOption(chart, rows, xKey, series, options) {
|
|
159
172
|
const AXIS = "#9aa4b2";
|
|
160
173
|
const titleBlock = options?.title ? { title: { text: options.title, left: "center", top: 2, textStyle: { color: AXIS, fontSize: 15, fontWeight: 600 } } } : {};
|
|
161
174
|
const hasTitle = !!options?.title;
|
|
162
|
-
if (chart === "pie") {
|
|
175
|
+
if (chart === "pie" || chart === "donut") {
|
|
163
176
|
const valueKey = series[0]?.key ?? "value";
|
|
164
177
|
return {
|
|
165
178
|
...titleBlock,
|
|
@@ -168,7 +181,7 @@ function toEChartsOption(chart, rows, xKey, series, options) {
|
|
|
168
181
|
series: [
|
|
169
182
|
{
|
|
170
183
|
type: "pie",
|
|
171
|
-
radius: ["45%", "72%"],
|
|
184
|
+
radius: chart === "donut" ? ["45%", "70%"] : ["45%", "72%"],
|
|
172
185
|
padAngle: 2,
|
|
173
186
|
itemStyle: { borderRadius: 4 },
|
|
174
187
|
label: { color: AXIS },
|
|
@@ -181,13 +194,20 @@ function toEChartsOption(chart, rows, xKey, series, options) {
|
|
|
181
194
|
]
|
|
182
195
|
};
|
|
183
196
|
}
|
|
197
|
+
const isNumeric = (v) => v !== void 0 && v !== "" && Number.isFinite(Number(v));
|
|
198
|
+
const scatterOnValueAxis = chart === "scatter" && rows.length > 0 && rows.every((r) => isNumeric(r[xKey]));
|
|
184
199
|
const stacked = (chart === "bar" || chart === "area") && !!options?.stacked;
|
|
185
200
|
return {
|
|
186
201
|
...titleBlock,
|
|
187
|
-
tooltip: { trigger: "axis" },
|
|
202
|
+
tooltip: { trigger: chart === "scatter" ? "item" : "axis" },
|
|
188
203
|
legend: { bottom: 0, textStyle: { color: AXIS } },
|
|
189
204
|
grid: { left: 8, right: 16, top: hasTitle ? 44 : 24, bottom: 40, containLabel: true },
|
|
190
|
-
xAxis: {
|
|
205
|
+
xAxis: scatterOnValueAxis ? {
|
|
206
|
+
type: "value",
|
|
207
|
+
axisLabel: { color: AXIS, fontSize: 12 },
|
|
208
|
+
axisLine: { lineStyle: { color: "#d4d7dd" } },
|
|
209
|
+
splitLine: { lineStyle: { color: "#e5e7eb", type: "dashed" } }
|
|
210
|
+
} : {
|
|
191
211
|
type: "category",
|
|
192
212
|
data: rows.map((r) => String(r[xKey] ?? "")),
|
|
193
213
|
axisLabel: { color: AXIS, fontSize: 12 },
|
|
@@ -200,15 +220,23 @@ function toEChartsOption(chart, rows, xKey, series, options) {
|
|
|
200
220
|
axisLabel: { color: AXIS, fontSize: 12 },
|
|
201
221
|
splitLine: { lineStyle: { color: "#e5e7eb", type: "dashed" } }
|
|
202
222
|
},
|
|
203
|
-
series: series.map(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
series: series.map(
|
|
224
|
+
(s, i) => chart === "scatter" ? {
|
|
225
|
+
name: s.label ?? s.key,
|
|
226
|
+
type: "scatter",
|
|
227
|
+
symbolSize: 10,
|
|
228
|
+
itemStyle: { color: seriesColor(s, i) },
|
|
229
|
+
data: scatterOnValueAxis ? rows.map((r) => [Number(r[xKey]), Number(r[s.key] ?? 0)]) : rows.map((r) => Number(r[s.key] ?? 0))
|
|
230
|
+
} : {
|
|
231
|
+
name: s.label ?? s.key,
|
|
232
|
+
type: chart === "area" ? "line" : chart,
|
|
233
|
+
...chart === "area" ? { areaStyle: { opacity: 0.2 }, smooth: false } : {},
|
|
234
|
+
...chart !== "bar" ? { showSymbol: false, lineStyle: { width: 2 } } : {},
|
|
235
|
+
stack: stacked ? "total" : void 0,
|
|
236
|
+
itemStyle: { color: seriesColor(s, i) },
|
|
237
|
+
data: rows.map((r) => Number(r[s.key] ?? 0))
|
|
238
|
+
}
|
|
239
|
+
)
|
|
212
240
|
};
|
|
213
241
|
}
|
|
214
242
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useArtifactPatch } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
import { useArtifactPatch } from './chunk-FSFOURG5.js';
|
|
2
|
+
import './chunk-ZLXAWRUP.js';
|
|
3
|
+
import { useT } from './chunk-QMOJEGRH.js';
|
|
3
4
|
import { useState, useRef } from 'react';
|
|
4
5
|
import ReactMarkdown from 'react-markdown';
|
|
5
6
|
import remarkGfm from 'remark-gfm';
|
|
@@ -10,6 +11,7 @@ function wordStats(text) {
|
|
|
10
11
|
return { words, minutes: Math.max(1, Math.round(words / 200)) };
|
|
11
12
|
}
|
|
12
13
|
function DocumentRenderer({ artifact }) {
|
|
14
|
+
const t = useT();
|
|
13
15
|
const { content } = artifact.data;
|
|
14
16
|
const patch = useArtifactPatch(artifact.id);
|
|
15
17
|
const [draft, setDraft] = useState(null);
|
|
@@ -46,15 +48,15 @@ function DocumentRenderer({ artifact }) {
|
|
|
46
48
|
const { words, minutes } = wordStats(editing ? draft ?? "" : content);
|
|
47
49
|
return /* @__PURE__ */ jsxs("div", { className: "cv-word", children: [
|
|
48
50
|
editing && /* @__PURE__ */ jsxs("div", { className: "cv-doc-bar cv-chrome", onMouseDown: (e) => e.preventDefault(), children: [
|
|
49
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
51
|
+
/* @__PURE__ */ jsx("button", { title: t("bold"), onMouseDown: (e) => {
|
|
50
52
|
e.preventDefault();
|
|
51
53
|
wrap("**");
|
|
52
54
|
}, children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
|
|
53
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
55
|
+
/* @__PURE__ */ jsx("button", { title: t("italic"), onMouseDown: (e) => {
|
|
54
56
|
e.preventDefault();
|
|
55
57
|
wrap("*");
|
|
56
58
|
}, children: /* @__PURE__ */ jsx("i", { children: "I" }) }),
|
|
57
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
59
|
+
/* @__PURE__ */ jsx("button", { title: t("inlineCode"), onMouseDown: (e) => {
|
|
58
60
|
e.preventDefault();
|
|
59
61
|
wrap("`");
|
|
60
62
|
}, children: "<>" }),
|
|
@@ -67,33 +69,36 @@ function DocumentRenderer({ artifact }) {
|
|
|
67
69
|
e.preventDefault();
|
|
68
70
|
prefixLines("## ");
|
|
69
71
|
}, children: "H2" }),
|
|
70
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
72
|
+
/* @__PURE__ */ jsx("button", { title: t("bulletList"), onMouseDown: (e) => {
|
|
71
73
|
e.preventDefault();
|
|
72
74
|
prefixLines("- ");
|
|
73
75
|
}, children: "\u2022" }),
|
|
74
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
76
|
+
/* @__PURE__ */ jsx("button", { title: t("numberedList"), onMouseDown: (e) => {
|
|
75
77
|
e.preventDefault();
|
|
76
78
|
prefixLines("1. ");
|
|
77
79
|
}, children: "1." }),
|
|
78
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
80
|
+
/* @__PURE__ */ jsx("button", { title: t("quote"), onMouseDown: (e) => {
|
|
79
81
|
e.preventDefault();
|
|
80
82
|
prefixLines("> ");
|
|
81
83
|
}, children: "\u275D" }),
|
|
82
|
-
/* @__PURE__ */ jsx("button", { title: "
|
|
84
|
+
/* @__PURE__ */ jsx("button", { title: t("link"), onMouseDown: (e) => {
|
|
83
85
|
e.preventDefault();
|
|
84
86
|
link();
|
|
85
87
|
}, children: "\u{1F517}" }),
|
|
86
88
|
/* @__PURE__ */ jsx("span", { className: "cv-doc-bar__spacer" }),
|
|
87
89
|
/* @__PURE__ */ jsxs("span", { className: "cv-doc-bar__count", children: [
|
|
88
90
|
words,
|
|
89
|
-
"
|
|
91
|
+
" ",
|
|
92
|
+
t("words"),
|
|
93
|
+
" \xB7 ",
|
|
90
94
|
minutes,
|
|
91
|
-
"
|
|
95
|
+
" ",
|
|
96
|
+
t("minRead")
|
|
92
97
|
] }),
|
|
93
98
|
/* @__PURE__ */ jsx("button", { className: "cv-doc-bar__done", onMouseDown: (e) => {
|
|
94
99
|
e.preventDefault();
|
|
95
100
|
commit();
|
|
96
|
-
}, children: "
|
|
101
|
+
}, children: t("doneEditing") })
|
|
97
102
|
] }),
|
|
98
103
|
/* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
|
|
99
104
|
"textarea",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useT } from './chunk-QMOJEGRH.js';
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
function dataUrlToBlob(url) {
|
|
6
|
+
const m = /^data:[^,]*?(;base64)?,(.*)$/s.exec(url);
|
|
7
|
+
if (!m) return null;
|
|
8
|
+
try {
|
|
9
|
+
if (m[1]) {
|
|
10
|
+
const bin = atob(m[2]);
|
|
11
|
+
const bytes = new Uint8Array(bin.length);
|
|
12
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
13
|
+
return new Blob([bytes], { type: "application/pdf" });
|
|
14
|
+
}
|
|
15
|
+
return new Blob([decodeURIComponent(m[2])], { type: "application/pdf" });
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function PdfRenderer({ artifact }) {
|
|
21
|
+
const t = useT();
|
|
22
|
+
const src = artifact.data.src;
|
|
23
|
+
const isData = typeof src === "string" && src.startsWith("data:");
|
|
24
|
+
const [blobUrl, setBlobUrl] = useState(null);
|
|
25
|
+
const [broken, setBroken] = useState(false);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setBroken(false);
|
|
28
|
+
if (!isData) {
|
|
29
|
+
setBlobUrl(null);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const blob = dataUrlToBlob(src);
|
|
33
|
+
if (!blob) {
|
|
34
|
+
setBroken(true);
|
|
35
|
+
setBlobUrl(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const url = URL.createObjectURL(blob);
|
|
39
|
+
setBlobUrl(url);
|
|
40
|
+
return () => URL.revokeObjectURL(url);
|
|
41
|
+
}, [src, isData]);
|
|
42
|
+
const viewUrl = isData ? blobUrl : src;
|
|
43
|
+
const filename = useMemo(
|
|
44
|
+
() => artifact.data.filename || `${artifact.title || "document"}.pdf`,
|
|
45
|
+
[artifact.data.filename, artifact.title]
|
|
46
|
+
);
|
|
47
|
+
const [zoom, setZoom] = useState("fit");
|
|
48
|
+
const zoomOut = () => setZoom((z) => Math.max(50, (z === "fit" ? 100 : z) - 25));
|
|
49
|
+
const zoomIn = () => setZoom((z) => Math.min(300, (z === "fit" ? 100 : z) + 25));
|
|
50
|
+
const frameSrc = viewUrl ? `${viewUrl}#toolbar=0&navpanes=0&zoom=${zoom === "fit" ? "page-width" : zoom}` : null;
|
|
51
|
+
const download = () => {
|
|
52
|
+
if (!viewUrl) return;
|
|
53
|
+
const a = document.createElement("a");
|
|
54
|
+
a.href = viewUrl;
|
|
55
|
+
a.download = filename;
|
|
56
|
+
a.click();
|
|
57
|
+
};
|
|
58
|
+
if (!src || broken) {
|
|
59
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: t("waitingPdf") });
|
|
60
|
+
}
|
|
61
|
+
if (!viewUrl) {
|
|
62
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: t("loading") });
|
|
63
|
+
}
|
|
64
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-pdf-panel", children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-pdf-tools", children: [
|
|
66
|
+
/* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__name", title: filename, children: filename }),
|
|
67
|
+
/* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__spacer" }),
|
|
68
|
+
/* @__PURE__ */ jsxs("span", { className: "cv-pdf-tools__zoom", children: [
|
|
69
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: zoomOut, title: t("zoomOut"), children: "\u2212" }),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
type: "button",
|
|
74
|
+
className: "cv-pdf-tools__zoom-label",
|
|
75
|
+
onClick: () => setZoom("fit"),
|
|
76
|
+
title: t("fitWidth"),
|
|
77
|
+
children: zoom === "fit" ? t("fitWidth") : `${zoom}%`
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: zoomIn, title: t("zoomIn"), children: "\uFF0B" })
|
|
81
|
+
] }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__sep" }),
|
|
83
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: download, title: t("download"), children: t("download") }),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
type: "button",
|
|
88
|
+
onClick: () => window.open(viewUrl, "_blank", "noopener"),
|
|
89
|
+
title: t("openBtn"),
|
|
90
|
+
children: t("openBtn")
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] }),
|
|
94
|
+
/* @__PURE__ */ jsx("div", { className: "cv-pdf", children: /* @__PURE__ */ jsx("iframe", { className: "cv-pdf__frame", src: frameSrc, title: filename }, frameSrc) })
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { PdfRenderer };
|