@braincrew-lab/langchain-canvas 0.3.0 → 0.4.9

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,11 +1,8 @@
1
1
  # @braincrew-lab/langchain-canvas
2
2
 
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)
3
+ LangChain 챗 앱을 위한 캔버스. 에이전트가 아티팩트(웹 페이지·스프레드시트·슬라이드·차트·문서)를 스트리밍하면 대화 옆에 실시간으로 뜨고 그 자리에서 편집됩니다. 사용자가 직접 손보고 실제 파일로 내보낼 수 있습니다.
5
4
 
6
- LangChain 앱을 위한 캔버스. 에이전트가 아티팩트(웹 페이지·스프레드시트·슬라이드·차트·문서·PDF)를 스트리밍하면 대화 옆에 실시간으로 뜨고 자리에서 편집됩니다. 사용자가 직접 손보고 실제 파일로 내보낼 수 있습니다.
7
-
8
- ChatGPT Canvas나 Claude Artifacts 같은 경험을, **내 React 앱에 넣고 내 에이전트에 연결하는 패키지**로 제공합니다. 스크린샷·아키텍처·와이어 프로토콜 전체는 [GitHub 레포](https://github.com/braincrew-lab/langchain-canvas)에 있습니다.
5
+ ChatGPT Canvas나 Claude Artifacts 같은 경험을, **내 React 앱에 넣고 에이전트에 연결하는 패키지**로 제공합니다.
9
6
 
10
7
  > [English README](./README.md)
11
8
 
@@ -77,29 +74,26 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
77
74
 
78
75
  정적 표가 아니라 스프레드시트 엔진(Fortune-sheet) 위에서 동작합니다.
79
76
 
80
- - **실시간 수식** — `=SUM(C2:C4)`, `=VLOOKUP(...)`, `=A2*B2` 입력 시 계산. 셀참조·범위·함수 자동완성 지원. 계산은 선택 설치 `fast-formula-parser`가 담당해 Excel 함수 세트(`IF`, `COUNTIF`, `SUMIF`, `INDEX`/`MATCH`, …)가 바로 동작
77
+ - **실시간 수식** — `=SUM(C2:C4)`, `=AVERAGE(...)`, `=A2*B2` 입력 시 계산. 셀참조·범위·함수 자동완성 지원
81
78
  - **데이터로 온 수식** — 에이전트가 값으로 보낸 수식(`"=AVERAGE(B2:B4)"`)은 **로드 시 미리 계산**되어 결과가 바로 표시
82
- - **Excel 스타일 리본** 삽입 / 글꼴 / 맞춤 / 표시 형식 / 편집 / 보기로 그룹된 트레이, **수식 바**, **Σ 자동 합계** 메뉴
83
- - **서식** — 폰트, 숫자/통화/%/날짜 서식, 굵게/기울임, 테두리, 셀 병합/해제, 정렬, **틀 고정**, 다중 시트, 그리고 에이전트가 만든 요란한 셀 색을 한 번에 걷어내는 **스타일 정리**
79
+ - **전체 툴바**폰트, 숫자/통화/% 서식, 굵게/기울임, 테두리, 병합, 정렬, 다중 시트 데스크톱 수준
84
80
  - 넓은 그리드에서 **상하좌우 부드러운 스크롤**
85
- - **Export** — `.xlsx`(폰트/병합/서식 포함) 또는 `.csv` — 둘 다 사용자의 그리드 편집이 반영됨
81
+ - **Export** — `.xlsx`(폰트/병합/서식 포함) 또는 `.csv`
86
82
 
87
83
  ### 🖼️ 슬라이드 (`slides`) — 프리 캔버스 덱
88
84
 
89
85
  모든 요소가 움직이는 PowerPoint 스타일 편집기.
90
86
 
91
- - **자유 배치** — 텍스트/이미지 요소 드래그·리사이즈, 드래그와 리사이즈 모두에서 스냅 가이드, 회전, Shift로 비율 고정 리사이즈
92
- - **다중 선택 & 그룹** — Shift+클릭 또는 마퀴 선택 후 그룹/해제(⌘G / ⌘⇧G). 떠 있는 액션 바로 정렬·분배·복제·삭제
93
- - **키보드 편집** — 방향키 미세 이동, ⌘D 복제, ⌘C/⌘V(슬라이드 간 가능), ⌘] / ⌘[ z-순서
87
+ - **자유 배치** — 텍스트/이미지 요소 드래그·리사이즈, 가이드 스냅
94
88
  - **인라인 편집** — 더블클릭 텍스트 편집, 굵게/크기/색/정렬 툴바
95
89
  - **구조** — 슬라이드 추가/복제/삭제/순서, 썸네일 레일, 발표자 노트
96
- - **8가지 테마**(Editorial · Gallery · Boardroom · Sage · Graphite · Observatory · Ultramarine · Bordeaux)·배경, 발표 모드(전체화면, 방향키 이동, 진행 표시)
97
- - **Export** — `.pptx` 또는 **PDF**(전 슬라이드)
90
+ - **테마·배경**, 발표 모드(전체화면, 방향키 이동)
91
+ - **Export** — `.pptx`, **Figma 복사**(에디터블 프레임으로 바로 붙여넣기), **PDF**(전 슬라이드)
98
92
 
99
93
  ### 📝 문서 (`document`) — Markdown / Word
100
94
 
101
95
  - **클릭 편집** — 페이지를 Markdown으로 편집(GFM 렌더)
102
- - **Export** — `.docx`, `.md`, `.hwpx`(한글), `.pdf`, `.html`
96
+ - **Export** — `.docx`, `.md`, `.pdf`, `.html`
103
97
 
104
98
  ### 📈 차트 (`chart`)
105
99
 
@@ -108,15 +102,10 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
108
102
  - 시리즈(또는 파이 조각)별 **색상 변경**, 시리즈 이름, Y축 라벨, 스택 토글
109
103
  - **Export** — `.pdf`(차트가 SVG라 선명) 또는 raw JSON
110
104
 
111
- ### 📄 PDF (`pdf`)
112
-
113
- - 에이전트가 `{ type: "pdf", data: { src } }`(`data:`/`blob:`/https URL)를 emit 하면 **브라우저 내장 뷰어**로 렌더 — 의존성 0, 줌 컨트롤 포함
114
- - `data:` 소스는 `application/pdf`로 고정되어, 조작된 아티팩트가 스크립트 실행 가능한 same-origin 프레임을 끼워 넣을 수 없음
115
-
116
105
  ### 📁 파일 — 라운드트립
117
106
 
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**(브라우저 인쇄)
107
+ - **Import** — 드래그앤드롭 또는 파일 선택: **CSV · Excel · Markdown · HTML · JSON** → 편집 가능한 아티팩트로
108
+ - **Export** — 각 아티팩트를 네이티브 포맷으로, 그리고 범용 **standalone `.html`** · **PDF**(브라우저 인쇄)
120
109
 
121
110
  ### 🧰 모든 아티팩트 공통
122
111
 
@@ -132,7 +121,6 @@ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이
132
121
  - **peerDependency:** React 18 또는 19 — 직접 제공. ESM 전용.
133
122
  - **스타일:** `import "@braincrew-lab/langchain-canvas/styles.css"` 한 번.
134
123
  - **인스턴스 격리:** `<CanvasProvider>`가 서브트리마다 독립 store 제공.
135
- - **한국어 UI:** `<Canvas locale="ko" />` 하나로 UI 전체가 한국어/영어 전환.
136
124
  - **커스텀 렌더러:** `registry`로 타입 렌더 방식 추가/오버라이드.
137
125
 
138
126
  ```tsx
package/README.md CHANGED
@@ -1,11 +1,8 @@
1
1
  # @braincrew-lab/langchain-canvas
2
2
 
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)
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.
5
4
 
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).
5
+ Think ChatGPT Canvas or Claude Artifacts, but a package you drop into your own React app and point at your own agent.
9
6
 
10
7
  > [한국어 README](./README.ko.md)
11
8
 
@@ -77,29 +74,26 @@ HTML is the base substrate, rendered in a sandboxed iframe. Selecting and editin
77
74
 
78
75
  Runs on a spreadsheet engine (Fortune-sheet), not a static table.
79
76
 
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.
77
+ - **Live formulas** — type `=SUM(C2:C4)`, `=AVERAGE(...)`, `=A2*B2`; they calculate, with cell references, ranges, and function autocomplete.
81
78
  - **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.
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.
79
+ - **Full toolbar** — fonts, number/currency/percent formats, bold/italic, borders, cell merging, alignment, multiple sheets like a desktop spreadsheet.
84
80
  - **Smooth scrolling** in both directions over a large grid.
85
- - **Export** to `.xlsx` (with fonts/merges/formats) or `.csv` — both include the user's in-grid edits.
81
+ - **Export** to `.xlsx` (with fonts/merges/formats) or `.csv`.
86
82
 
87
83
  ### 🖼️ Slides (`slides`) — a free-canvas deck
88
84
 
89
85
  A PowerPoint-style editor where every element is movable.
90
86
 
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.
87
+ - **Free positioning** — drag and resize text/image elements; snap to guides.
94
88
  - **Inline editing** — double-click to edit text; format toolbar for bold/size/color/align.
95
89
  - **Structure** — add/duplicate/delete/reorder slides, thumbnails rail, speaker notes.
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).
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).
98
92
 
99
93
  ### 📝 Documents (`document`) — Markdown / Word
100
94
 
101
95
  - **Click-to-edit** the page as Markdown, rendered with GFM.
102
- - **Export** to `.docx`, `.md`, `.hwpx` (한글), `.pdf`, or `.html`.
96
+ - **Export** to `.docx`, `.md`, `.pdf`, or `.html`.
103
97
 
104
98
  ### 📈 Charts (`chart`)
105
99
 
@@ -108,15 +102,10 @@ A PowerPoint-style editor where every element is movable.
108
102
  - **Recolor** each series (or each pie slice), rename series, set the y-axis label, toggle stacking.
109
103
  - **Export** to `.pdf` (the chart is SVG, so it prints crisply) or the raw JSON.
110
104
 
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
-
116
105
  ### 📁 Files — round-trip
117
106
 
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).
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).
120
109
 
121
110
  ### 🧰 Across every artifact
122
111
 
@@ -132,7 +121,6 @@ A PowerPoint-style editor where every element is movable.
132
121
  - **Peer dependency:** React 18 or 19 — you bring your own. ESM only.
133
122
  - **Styles:** `import "@braincrew-lab/langchain-canvas/styles.css"` once.
134
123
  - **Isolated instances:** `<CanvasProvider>` gives each subtree its own store.
135
- - **Korean UI:** `<Canvas locale="ko" />` localizes the entire chrome (English / Korean).
136
124
  - **Bring your own renderer:** pass `registry` to add or override how a type renders.
137
125
 
138
126
  ```tsx
@@ -1,18 +1,17 @@
1
- import { useArtifactPatch } from './chunk-FSFOURG5.js';
2
- import './chunk-ZLXAWRUP.js';
3
- import { useT } from './chunk-QMOJEGRH.js';
1
+ import { useArtifactPatch } from './chunk-K2UZAYW2.js';
2
+ import './chunk-EHW446VF.js';
4
3
  import { useState, useRef, useMemo, useEffect } from 'react';
5
4
  import * as echarts from 'echarts/core';
6
- import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
5
+ import { BarChart, LineChart, PieChart } from 'echarts/charts';
7
6
  import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
8
7
  import { CanvasRenderer } from 'echarts/renderers';
9
8
  import { jsxs, jsx } from 'react/jsx-runtime';
10
9
 
11
- echarts.use([BarChart, LineChart, PieChart, ScatterChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
10
+ echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
12
11
  var PALETTE = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#0ea5e9", "#a855f7"];
13
12
  var seriesColor = (series, index) => series.color ?? PALETTE[index % PALETTE.length];
14
13
  var sliceColor = (options, index) => options?.colors?.[index] ?? PALETTE[index % PALETTE.length];
15
- var CHART_TYPES = ["bar", "line", "area", "scatter", "pie", "donut"];
14
+ var CHART_TYPES = ["bar", "line", "area", "pie"];
16
15
  function EChart({ option, height, onInst }) {
17
16
  const boxRef = useRef(null);
18
17
  const instRef = useRef(null);
@@ -36,7 +35,6 @@ function EChart({ option, height, onInst }) {
36
35
  return /* @__PURE__ */ jsx("div", { ref: boxRef, className: "cv-chart__canvas", style: { width: "100%", height } });
37
36
  }
38
37
  function ChartRenderer({ artifact }) {
39
- const t = useT();
40
38
  const { chart, rows, xKey, series, options, echartsOption } = artifact.data;
41
39
  const patch = useArtifactPatch(artifact.id);
42
40
  const [editing, setEditing] = useState(false);
@@ -58,15 +56,15 @@ function ChartRenderer({ artifact }) {
58
56
  return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
59
57
  /* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
60
58
  /* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
61
- /* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: t("downloadPng"), children: t("downloadPng") })
59
+ /* @__PURE__ */ jsx("button", { className: "cv-edit-btn", onClick: downloadPng, title: "Download as PNG", children: "\u2913 PNG" })
62
60
  ] }),
63
61
  /* @__PURE__ */ jsx(EChart, { option, height: 320, onInst: (i) => instRef.current = i })
64
62
  ] });
65
63
  }
66
64
  if (rows.length === 0) {
67
- return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: t("waitingData") });
65
+ return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: "Waiting for data\u2026" });
68
66
  }
69
- const isPie = chart === "pie" || chart === "donut";
67
+ const isPie = chart === "pie";
70
68
  const setSeries = (i, partial) => patch({ series: series.map((s, j) => j === i ? { ...s, ...partial } : s) });
71
69
  const setSliceColor = (i, color) => {
72
70
  const colors = rows.map((_, j) => options?.colors?.[j] ?? PALETTE[j % PALETTE.length]);
@@ -82,38 +80,38 @@ function ChartRenderer({ artifact }) {
82
80
  const removeRow = (i) => patch({ rows: rows.filter((_, j) => j !== i) });
83
81
  return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
84
82
  /* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
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)) }),
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)) }),
86
84
  (chart === "bar" || chart === "area") && /* @__PURE__ */ jsxs("label", { className: "cv-chart__stack", children: [
87
85
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: !!options?.stacked, onChange: (e) => patch({ options: { ...options, stacked: e.target.checked } }) }),
88
- t("stacked")
86
+ "Stacked"
89
87
  ] }),
90
88
  /* @__PURE__ */ jsx(
91
89
  "input",
92
90
  {
93
91
  className: "cv-chart__title-input",
94
92
  value: options?.title ?? "",
95
- placeholder: t("chartTitle"),
93
+ placeholder: "Chart title\u2026",
96
94
  onChange: (e) => patch({ options: { ...options, title: e.target.value || void 0 } }),
97
- title: t("chartTitle")
95
+ title: "Chart title"
98
96
  }
99
97
  ),
100
98
  /* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
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") })
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" })
103
101
  ] }),
104
102
  editing && /* @__PURE__ */ jsxs("div", { className: "cv-chart__editor cv-chrome", children: [
105
103
  /* @__PURE__ */ jsx("div", { className: "cv-chart__legend", children: isPie ? rows.map((r, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
106
104
  /* @__PURE__ */ jsx("input", { type: "color", value: sliceColor(options, i), onChange: (e) => setSliceColor(i, e.target.value), title: `Color: ${r[xKey]}` }),
107
105
  /* @__PURE__ */ jsx("span", { className: "cv-chart__swatch-label", children: String(r[xKey]) })
108
106
  ] }, i)) : series.map((s, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
109
- /* @__PURE__ */ jsx("input", { type: "color", value: seriesColor(s, i), onChange: (e) => setSeries(i, { color: e.target.value }), title: t("seriesColor") }),
107
+ /* @__PURE__ */ jsx("input", { type: "color", value: seriesColor(s, i), onChange: (e) => setSeries(i, { color: e.target.value }), title: "Series color" }),
110
108
  /* @__PURE__ */ jsx(
111
109
  "input",
112
110
  {
113
111
  className: "cv-chart__series-name",
114
112
  value: s.label ?? s.key,
115
113
  onChange: (e) => setSeries(i, { label: e.target.value }),
116
- title: t("seriesName")
114
+ title: "Series name"
117
115
  }
118
116
  )
119
117
  ] }, s.key)) }),
@@ -126,23 +124,6 @@ function ChartRenderer({ artifact }) {
126
124
  /* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340, onInst: (i) => instRef.current = i })
127
125
  ] });
128
126
  }
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
- }
146
127
  function DataGrid({
147
128
  rows,
148
129
  xKey,
@@ -151,7 +132,6 @@ function DataGrid({
151
132
  onAddRow,
152
133
  onRemoveRow
153
134
  }) {
154
- const t = useT();
155
135
  return /* @__PURE__ */ jsxs("div", { className: "cv-chart__grid", children: [
156
136
  /* @__PURE__ */ jsxs("table", { children: [
157
137
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
@@ -161,18 +141,25 @@ function DataGrid({
161
141
  ] }) }),
162
142
  /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
163
143
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { value: String(row[xKey] ?? ""), onChange: (e) => onCell(i, xKey, e.target.value) }) }),
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" }) })
144
+ series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
145
+ "input",
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" }) })
166
153
  ] }, i)) })
167
154
  ] }),
168
- /* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: t("addRowBtn") })
155
+ /* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: "+ Add row" })
169
156
  ] });
170
157
  }
171
158
  function toEChartsOption(chart, rows, xKey, series, options) {
172
159
  const AXIS = "#9aa4b2";
173
160
  const titleBlock = options?.title ? { title: { text: options.title, left: "center", top: 2, textStyle: { color: AXIS, fontSize: 15, fontWeight: 600 } } } : {};
174
161
  const hasTitle = !!options?.title;
175
- if (chart === "pie" || chart === "donut") {
162
+ if (chart === "pie") {
176
163
  const valueKey = series[0]?.key ?? "value";
177
164
  return {
178
165
  ...titleBlock,
@@ -181,7 +168,7 @@ function toEChartsOption(chart, rows, xKey, series, options) {
181
168
  series: [
182
169
  {
183
170
  type: "pie",
184
- radius: chart === "donut" ? ["45%", "70%"] : ["45%", "72%"],
171
+ radius: ["45%", "72%"],
185
172
  padAngle: 2,
186
173
  itemStyle: { borderRadius: 4 },
187
174
  label: { color: AXIS },
@@ -194,20 +181,13 @@ function toEChartsOption(chart, rows, xKey, series, options) {
194
181
  ]
195
182
  };
196
183
  }
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]));
199
184
  const stacked = (chart === "bar" || chart === "area") && !!options?.stacked;
200
185
  return {
201
186
  ...titleBlock,
202
- tooltip: { trigger: chart === "scatter" ? "item" : "axis" },
187
+ tooltip: { trigger: "axis" },
203
188
  legend: { bottom: 0, textStyle: { color: AXIS } },
204
189
  grid: { left: 8, right: 16, top: hasTitle ? 44 : 24, bottom: 40, containLabel: true },
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
- } : {
190
+ xAxis: {
211
191
  type: "category",
212
192
  data: rows.map((r) => String(r[xKey] ?? "")),
213
193
  axisLabel: { color: AXIS, fontSize: 12 },
@@ -220,23 +200,15 @@ function toEChartsOption(chart, rows, xKey, series, options) {
220
200
  axisLabel: { color: AXIS, fontSize: 12 },
221
201
  splitLine: { lineStyle: { color: "#e5e7eb", type: "dashed" } }
222
202
  },
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
- )
203
+ series: series.map((s, i) => ({
204
+ name: s.label ?? s.key,
205
+ type: chart === "area" ? "line" : chart,
206
+ ...chart === "area" ? { areaStyle: { opacity: 0.2 }, smooth: false } : {},
207
+ ...chart !== "bar" ? { showSymbol: false, lineStyle: { width: 2 } } : {},
208
+ stack: stacked ? "total" : void 0,
209
+ itemStyle: { color: seriesColor(s, i) },
210
+ data: rows.map((r) => Number(r[s.key] ?? 0))
211
+ }))
240
212
  };
241
213
  }
242
214
 
@@ -1,8 +1,9 @@
1
- import { useArtifactPatch } from './chunk-FSFOURG5.js';
2
- import './chunk-ZLXAWRUP.js';
3
- import { useT } from './chunk-QMOJEGRH.js';
1
+ import { useAssetUrl } from './chunk-FTNRRJ3K.js';
2
+ import './chunk-7T5DRR3F.js';
3
+ import { useArtifactPatch } from './chunk-K2UZAYW2.js';
4
+ import './chunk-EHW446VF.js';
4
5
  import { useState, useRef } from 'react';
5
- import ReactMarkdown from 'react-markdown';
6
+ import ReactMarkdown, { defaultUrlTransform } from 'react-markdown';
6
7
  import remarkGfm from 'remark-gfm';
7
8
  import { jsxs, jsx } from 'react/jsx-runtime';
8
9
 
@@ -11,12 +12,13 @@ function wordStats(text) {
11
12
  return { words, minutes: Math.max(1, Math.round(words / 200)) };
12
13
  }
13
14
  function DocumentRenderer({ artifact }) {
14
- const t = useT();
15
15
  const { content } = artifact.data;
16
16
  const patch = useArtifactPatch(artifact.id);
17
17
  const [draft, setDraft] = useState(null);
18
18
  const taRef = useRef(null);
19
19
  const editing = draft !== null;
20
+ const assetUrl = useAssetUrl();
21
+ const urlTransform = (url) => defaultUrlTransform(assetUrl(url) ?? url);
20
22
  const commit = () => {
21
23
  if (draft !== null && draft !== content) patch({ content: draft });
22
24
  setDraft(null);
@@ -48,15 +50,15 @@ function DocumentRenderer({ artifact }) {
48
50
  const { words, minutes } = wordStats(editing ? draft ?? "" : content);
49
51
  return /* @__PURE__ */ jsxs("div", { className: "cv-word", children: [
50
52
  editing && /* @__PURE__ */ jsxs("div", { className: "cv-doc-bar cv-chrome", onMouseDown: (e) => e.preventDefault(), children: [
51
- /* @__PURE__ */ jsx("button", { title: t("bold"), onMouseDown: (e) => {
53
+ /* @__PURE__ */ jsx("button", { title: "Bold", onMouseDown: (e) => {
52
54
  e.preventDefault();
53
55
  wrap("**");
54
56
  }, children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
55
- /* @__PURE__ */ jsx("button", { title: t("italic"), onMouseDown: (e) => {
57
+ /* @__PURE__ */ jsx("button", { title: "Italic", onMouseDown: (e) => {
56
58
  e.preventDefault();
57
59
  wrap("*");
58
60
  }, children: /* @__PURE__ */ jsx("i", { children: "I" }) }),
59
- /* @__PURE__ */ jsx("button", { title: t("inlineCode"), onMouseDown: (e) => {
61
+ /* @__PURE__ */ jsx("button", { title: "Inline code", onMouseDown: (e) => {
60
62
  e.preventDefault();
61
63
  wrap("`");
62
64
  }, children: "<>" }),
@@ -69,36 +71,33 @@ function DocumentRenderer({ artifact }) {
69
71
  e.preventDefault();
70
72
  prefixLines("## ");
71
73
  }, children: "H2" }),
72
- /* @__PURE__ */ jsx("button", { title: t("bulletList"), onMouseDown: (e) => {
74
+ /* @__PURE__ */ jsx("button", { title: "Bullet list", onMouseDown: (e) => {
73
75
  e.preventDefault();
74
76
  prefixLines("- ");
75
77
  }, children: "\u2022" }),
76
- /* @__PURE__ */ jsx("button", { title: t("numberedList"), onMouseDown: (e) => {
78
+ /* @__PURE__ */ jsx("button", { title: "Numbered list", onMouseDown: (e) => {
77
79
  e.preventDefault();
78
80
  prefixLines("1. ");
79
81
  }, children: "1." }),
80
- /* @__PURE__ */ jsx("button", { title: t("quote"), onMouseDown: (e) => {
82
+ /* @__PURE__ */ jsx("button", { title: "Quote", onMouseDown: (e) => {
81
83
  e.preventDefault();
82
84
  prefixLines("> ");
83
85
  }, children: "\u275D" }),
84
- /* @__PURE__ */ jsx("button", { title: t("link"), onMouseDown: (e) => {
86
+ /* @__PURE__ */ jsx("button", { title: "Link", onMouseDown: (e) => {
85
87
  e.preventDefault();
86
88
  link();
87
89
  }, children: "\u{1F517}" }),
88
90
  /* @__PURE__ */ jsx("span", { className: "cv-doc-bar__spacer" }),
89
91
  /* @__PURE__ */ jsxs("span", { className: "cv-doc-bar__count", children: [
90
92
  words,
91
- " ",
92
- t("words"),
93
- " \xB7 ",
93
+ " words \xB7 ",
94
94
  minutes,
95
- " ",
96
- t("minRead")
95
+ " min read"
97
96
  ] }),
98
97
  /* @__PURE__ */ jsx("button", { className: "cv-doc-bar__done", onMouseDown: (e) => {
99
98
  e.preventDefault();
100
99
  commit();
101
- }, children: t("doneEditing") })
100
+ }, children: "Done" })
102
101
  ] }),
103
102
  /* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
104
103
  "textarea",
@@ -117,7 +116,7 @@ function DocumentRenderer({ artifact }) {
117
116
  title: "Click to edit",
118
117
  onClick: () => artifact.status !== "streaming" && setDraft(content),
119
118
  children: [
120
- /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }),
119
+ /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], urlTransform, children: content }),
121
120
  artifact.status === "streaming" && /* @__PURE__ */ jsx("span", { className: "cv-caret", "aria-hidden": true })
122
121
  ]
123
122
  }
@@ -0,0 +1,39 @@
1
+ import { isAssetReference, resolveAssetUrl } from './chunk-7T5DRR3F.js';
2
+ import { useCanvasStore } from './chunk-EHW446VF.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ function formatSize(size) {
6
+ if (typeof size !== "number") return null;
7
+ if (size < 1024) return `${size} B`;
8
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
9
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
10
+ }
11
+ function iconFor(mediaType, name) {
12
+ if (mediaType?.startsWith("image/")) return "\u{1F5BC}\uFE0F";
13
+ if (mediaType === "application/pdf") return "\u{1F4D5}";
14
+ const lower = name.toLowerCase();
15
+ if (lower.endsWith(".docx") || lower.endsWith(".doc")) return "\u{1F4C4}";
16
+ if (lower.endsWith(".xlsx") || lower.endsWith(".csv")) return "\u{1F522}";
17
+ if (lower.endsWith(".pptx") || lower.endsWith(".ppt")) return "\u{1F4FD}\uFE0F";
18
+ return "\u{1F4CE}";
19
+ }
20
+ function FileRenderer({ artifact }) {
21
+ const { path, name, mediaType, size, cover, excerpt, detail } = artifact.data;
22
+ const assetBaseUrl = useCanvasStore((s) => s.assetBaseUrl);
23
+ const href = assetBaseUrl && isAssetReference(path) ? resolveAssetUrl(path, assetBaseUrl) : null;
24
+ const isImage = Boolean(mediaType?.startsWith("image/"));
25
+ const facts = [mediaType, formatSize(size), detail].filter(Boolean).join(" \xB7 ");
26
+ return /* @__PURE__ */ jsxs("div", { className: "cv-file", children: [
27
+ isImage && href ? /* @__PURE__ */ jsx("img", { className: "cv-file__image", src: href, alt: name }) : cover ? /* @__PURE__ */ jsx("img", { className: "cv-file__cover", src: cover, alt: `${name} \u2014 first page` }) : excerpt ? /* @__PURE__ */ jsx("pre", { className: "cv-file__excerpt", children: excerpt }) : null,
28
+ /* @__PURE__ */ jsxs("div", { className: "cv-file__card", children: [
29
+ /* @__PURE__ */ jsx("span", { className: "cv-file__icon", "aria-hidden": true, children: iconFor(mediaType, name) }),
30
+ /* @__PURE__ */ jsxs("span", { className: "cv-file__meta", children: [
31
+ /* @__PURE__ */ jsx("b", { children: name }),
32
+ facts && /* @__PURE__ */ jsx("span", { className: "cv-file__facts", children: facts })
33
+ ] }),
34
+ href && /* @__PURE__ */ jsx("a", { className: "cv-file__download", href, download: name, children: "Download" })
35
+ ] })
36
+ ] });
37
+ }
38
+
39
+ export { FileRenderer };