@braincrew-lab/langchain-canvas 0.2.0 → 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 CHANGED
@@ -1,8 +1,11 @@
1
1
  # @braincrew-lab/langchain-canvas
2
2
 
3
- LangChain 챗 앱을 위한 캔버스. 에이전트가 아티팩트(웹 페이지·스프레드시트·슬라이드·차트·문서)를 스트리밍하면 대화 옆에 실시간으로 뜨고 그 자리에서 편집됩니다. 사용자가 직접 손보고 실제 파일로 내보낼 수 있습니다.
3
+ [![npm](https://img.shields.io/npm/v/%40braincrew-lab%2Flangchain-canvas?label=npm&color=cb3837)](https://www.npmjs.com/package/@braincrew-lab/langchain-canvas)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/braincrew-lab/langchain-canvas/blob/main/LICENSE)
4
5
 
5
- ChatGPT Canvas나 Claude Artifacts 같은 경험을, **내 React 앱에 넣고 에이전트에 연결하는 패키지**로 제공합니다.
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)`, `=AVERAGE(...)`, `=A2*B2` 입력 시 계산. 셀참조·범위·함수 자동완성 지원
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`, **Figma 복사**(에디터블 프레임으로 바로 붙여넣기), **PDF**(전 슬라이드)
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** — 각 아티팩트를 네이티브 포맷으로, 그리고 범용 **standalone `.html`** · **PDF**(브라우저 인쇄)
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
- A canvas for your LangChain chat app. Your agent streams an artifact — a web page, a spreadsheet, a slide deck, a chart, a document — and it shows up next to the conversation, live and editable. Users can tweak it by hand and export it to a real file.
3
+ [![npm](https://img.shields.io/npm/v/%40braincrew-lab%2Flangchain-canvas?label=npm&color=cb3837)](https://www.npmjs.com/package/@braincrew-lab/langchain-canvas)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/braincrew-lab/langchain-canvas/blob/main/LICENSE)
4
5
 
5
- Think ChatGPT Canvas or Claude Artifacts, but a package you drop into your own React app and point at your own agent.
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)`, `=AVERAGE(...)`, `=A2*B2`; they calculate, with cell references, ranges, and function autocomplete.
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
- - **Full toolbar** — fonts, number/currency/percent formats, bold/italic, borders, cell merging, alignment, multiple sheets like a desktop spreadsheet.
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 guides.
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
- - **Themes & backgrounds**, present mode (full-screen, arrow-key navigation).
91
- - **Export** to `.pptx`, or copy to **Figma** (paste straight in as editable frames), or **PDF** (all slides).
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-UL5F66PN.js';
2
- import './chunk-S54GJDSJ.js';
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: "Download as PNG", children: "\u2913 PNG" })
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: "Waiting for data\u2026" });
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((t) => /* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${chart === t ? "is-primary" : ""}`, onClick: () => patch({ chart: t }), children: t }, t)) }),
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
- "Stacked"
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: "Chart title\u2026",
95
+ placeholder: t("chartTitle"),
94
96
  onChange: (e) => patch({ options: { ...options, title: e.target.value || void 0 } }),
95
- title: "Chart 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: "Download as PNG", children: "\u2913 PNG" }),
100
- /* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${editing ? "is-primary" : ""}`, onClick: () => setEditing((v) => !v), children: editing ? "Done" : "Edit data" })
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: "Series color" }),
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: "Series name"
116
+ title: t("seriesName")
115
117
  }
116
118
  )
117
119
  ] }, s.key)) }),
@@ -149,6 +151,7 @@ function DataGrid({
149
151
  onAddRow,
150
152
  onRemoveRow
151
153
  }) {
154
+ const t = useT();
152
155
  return /* @__PURE__ */ jsxs("div", { className: "cv-chart__grid", children: [
153
156
  /* @__PURE__ */ jsxs("table", { children: [
154
157
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
@@ -159,17 +162,17 @@ function DataGrid({
159
162
  /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
160
163
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { value: String(row[xKey] ?? ""), onChange: (e) => onCell(i, xKey, e.target.value) }) }),
161
164
  series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(NumberCell, { value: row[s.key], onCommit: (n) => onCell(i, s.key, n) }) }, s.key)),
162
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("button", { className: "cv-chart__row-del", onClick: () => onRemoveRow(i), disabled: rows.length <= 1, title: "Remove row", children: "\xD7" }) })
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" }) })
163
166
  ] }, i)) })
164
167
  ] }),
165
- /* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: "+ Add row" })
168
+ /* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: t("addRowBtn") })
166
169
  ] });
167
170
  }
168
171
  function toEChartsOption(chart, rows, xKey, series, options) {
169
172
  const AXIS = "#9aa4b2";
170
173
  const titleBlock = options?.title ? { title: { text: options.title, left: "center", top: 2, textStyle: { color: AXIS, fontSize: 15, fontWeight: 600 } } } : {};
171
174
  const hasTitle = !!options?.title;
172
- if (chart === "pie") {
175
+ if (chart === "pie" || chart === "donut") {
173
176
  const valueKey = series[0]?.key ?? "value";
174
177
  return {
175
178
  ...titleBlock,
@@ -178,7 +181,7 @@ function toEChartsOption(chart, rows, xKey, series, options) {
178
181
  series: [
179
182
  {
180
183
  type: "pie",
181
- radius: ["45%", "72%"],
184
+ radius: chart === "donut" ? ["45%", "70%"] : ["45%", "72%"],
182
185
  padAngle: 2,
183
186
  itemStyle: { borderRadius: 4 },
184
187
  label: { color: AXIS },
@@ -191,13 +194,20 @@ function toEChartsOption(chart, rows, xKey, series, options) {
191
194
  ]
192
195
  };
193
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]));
194
199
  const stacked = (chart === "bar" || chart === "area") && !!options?.stacked;
195
200
  return {
196
201
  ...titleBlock,
197
- tooltip: { trigger: "axis" },
202
+ tooltip: { trigger: chart === "scatter" ? "item" : "axis" },
198
203
  legend: { bottom: 0, textStyle: { color: AXIS } },
199
204
  grid: { left: 8, right: 16, top: hasTitle ? 44 : 24, bottom: 40, containLabel: true },
200
- 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
+ } : {
201
211
  type: "category",
202
212
  data: rows.map((r) => String(r[xKey] ?? "")),
203
213
  axisLabel: { color: AXIS, fontSize: 12 },
@@ -210,15 +220,23 @@ function toEChartsOption(chart, rows, xKey, series, options) {
210
220
  axisLabel: { color: AXIS, fontSize: 12 },
211
221
  splitLine: { lineStyle: { color: "#e5e7eb", type: "dashed" } }
212
222
  },
213
- series: series.map((s, i) => ({
214
- name: s.label ?? s.key,
215
- type: chart === "area" ? "line" : chart,
216
- ...chart === "area" ? { areaStyle: { opacity: 0.2 }, smooth: false } : {},
217
- ...chart !== "bar" ? { showSymbol: false, lineStyle: { width: 2 } } : {},
218
- stack: stacked ? "total" : void 0,
219
- itemStyle: { color: seriesColor(s, i) },
220
- data: rows.map((r) => Number(r[s.key] ?? 0))
221
- }))
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
+ )
222
240
  };
223
241
  }
224
242
 
@@ -1,5 +1,6 @@
1
- import { useArtifactPatch } from './chunk-UL5F66PN.js';
2
- import './chunk-S54GJDSJ.js';
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: "Bold", onMouseDown: (e) => {
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: "Italic", onMouseDown: (e) => {
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: "Inline code", onMouseDown: (e) => {
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: "Bullet list", onMouseDown: (e) => {
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: "Numbered list", onMouseDown: (e) => {
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: "Quote", onMouseDown: (e) => {
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: "Link", onMouseDown: (e) => {
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
- " words \xB7 ",
91
+ " ",
92
+ t("words"),
93
+ " \xB7 ",
90
94
  minutes,
91
- " min read"
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: "Done" })
101
+ }, children: t("doneEditing") })
97
102
  ] }),
98
103
  /* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
99
104
  "textarea",
@@ -1,7 +1,7 @@
1
+ import { useT } from './chunk-QMOJEGRH.js';
1
2
  import { useState, useEffect, useMemo } from 'react';
2
3
  import { jsx, jsxs } from 'react/jsx-runtime';
3
4
 
4
- // src/components/renderers/PdfRenderer.tsx
5
5
  function dataUrlToBlob(url) {
6
6
  const m = /^data:[^,]*?(;base64)?,(.*)$/s.exec(url);
7
7
  if (!m) return null;
@@ -18,6 +18,7 @@ function dataUrlToBlob(url) {
18
18
  }
19
19
  }
20
20
  function PdfRenderer({ artifact }) {
21
+ const t = useT();
21
22
  const src = artifact.data.src;
22
23
  const isData = typeof src === "string" && src.startsWith("data:");
23
24
  const [blobUrl, setBlobUrl] = useState(null);
@@ -55,38 +56,38 @@ function PdfRenderer({ artifact }) {
55
56
  a.click();
56
57
  };
57
58
  if (!src || broken) {
58
- return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: "Waiting for PDF\u2026" });
59
+ return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: t("waitingPdf") });
59
60
  }
60
61
  if (!viewUrl) {
61
- return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: "Loading PDF\u2026" });
62
+ return /* @__PURE__ */ jsx("div", { className: "cv-pdf cv-pdf--empty", children: t("loading") });
62
63
  }
63
64
  return /* @__PURE__ */ jsxs("div", { className: "cv-pdf-panel", children: [
64
65
  /* @__PURE__ */ jsxs("div", { className: "cv-pdf-tools", children: [
65
66
  /* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__name", title: filename, children: filename }),
66
67
  /* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__spacer" }),
67
68
  /* @__PURE__ */ jsxs("span", { className: "cv-pdf-tools__zoom", children: [
68
- /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomOut, title: "Zoom out", children: "\u2212" }),
69
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomOut, title: t("zoomOut"), children: "\u2212" }),
69
70
  /* @__PURE__ */ jsx(
70
71
  "button",
71
72
  {
72
73
  type: "button",
73
74
  className: "cv-pdf-tools__zoom-label",
74
75
  onClick: () => setZoom("fit"),
75
- title: "Fit to width",
76
- children: zoom === "fit" ? "Fit" : `${zoom}%`
76
+ title: t("fitWidth"),
77
+ children: zoom === "fit" ? t("fitWidth") : `${zoom}%`
77
78
  }
78
79
  ),
79
- /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomIn, title: "Zoom in", children: "\uFF0B" })
80
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomIn, title: t("zoomIn"), children: "\uFF0B" })
80
81
  ] }),
81
82
  /* @__PURE__ */ jsx("span", { className: "cv-pdf-tools__sep" }),
82
- /* @__PURE__ */ jsx("button", { type: "button", onClick: download, title: "Download PDF", children: "\u2913 Download" }),
83
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: download, title: t("download"), children: t("download") }),
83
84
  /* @__PURE__ */ jsx(
84
85
  "button",
85
86
  {
86
87
  type: "button",
87
88
  onClick: () => window.open(viewUrl, "_blank", "noopener"),
88
- title: "Open in a new tab",
89
- children: "\u2197 Open"
89
+ title: t("openBtn"),
90
+ children: t("openBtn")
90
91
  }
91
92
  )
92
93
  ] }),