@braincrew-lab/langchain-canvas 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 langchain-canvas contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,170 @@
1
+ # @braincrew-lab/langchain-canvas
2
+
3
+ LangChain 챗 앱을 위한 캔버스. 에이전트가 아티팩트(웹 페이지·스프레드시트·슬라이드·차트·문서)를 스트리밍하면 대화 옆에 실시간으로 뜨고 그 자리에서 편집됩니다. 사용자가 직접 손보고 실제 파일로 내보낼 수 있습니다.
4
+
5
+ ChatGPT Canvas나 Claude Artifacts 같은 경험을, **내 React 앱에 넣고 내 에이전트에 연결하는 패키지**로 제공합니다.
6
+
7
+ > [English README](./README.md)
8
+
9
+ ```tsx
10
+ const { sendMessage, messages, canvas } = useCanvasStream({ endpoint: "/api/chat" });
11
+ // 사용자: "요금제 3개 비교 표 만들어줘"
12
+ // → <Canvas /> 에 진짜 스프레드시트가, 수식까지 계산되어 나타남
13
+ ```
14
+
15
+ ## 설치
16
+
17
+ ```bash
18
+ npm i @braincrew-lab/langchain-canvas
19
+ ```
20
+
21
+ Office 포맷 import/export는 선택적 peer 패키지를 씁니다 — 필요한 것만 설치하세요:
22
+
23
+ ```bash
24
+ npm i exceljs docx pptxgenjs fast-formula-parser
25
+ ```
26
+
27
+ 미설치 시 해당 export만 "무엇을 설치하라"고 안내하고, 나머지는 정상 동작합니다.
28
+
29
+ ## 마운트
30
+
31
+ 두 가지만 있으면 됩니다: 에이전트 스트림을 받는 훅과, 그걸 렌더하는 패널. store를 공유하므로 둘 사이 배선이 없습니다.
32
+
33
+ ```tsx
34
+ import { Canvas, useCanvasStream } from "@braincrew-lab/langchain-canvas";
35
+ import "@braincrew-lab/langchain-canvas/styles.css";
36
+
37
+ function App() {
38
+ const { sendMessage, messages } = useCanvasStream({ endpoint: "/api/chat" });
39
+ return <Canvas />; // sendMessage/messages는 당신의 챗 UI에서 렌더
40
+ }
41
+ ```
42
+
43
+ - `useCanvasStream({ endpoint })` → `{ sendMessage, messages, canvas, isStreaming, editSelection }`
44
+ - `<Canvas />`는 `"use client"` 포함 → **Next.js App Router** 파일에 그대로 사용
45
+ - `/api/chat`는 와이어 프로토콜(`canvas.create` / `append` / `patch` / `replace`)로 SSE 스트리밍. 동봉 Python 패키지가 LangChain/LangGraph 에이전트에서 이 프레임을 내보내고, 같은 JSON을 보내는 어떤 백엔드든 동작.
46
+
47
+ 셋업은 이게 전부입니다. 이후는 **각 기능 사용법** — 전부 사용자가 렌더된 아티팩트에서 직접 조작하는 것이고, 당신 쪽 추가 코드는 없습니다.
48
+
49
+ ---
50
+
51
+ ## 기능과 사용법
52
+
53
+ ### 🌐 웹 페이지 (`html`) — 비주얼 페이지 빌더
54
+
55
+ HTML이 기본 substrate이고, 샌드박스 iframe에 렌더됩니다. 에이전트가 만든 어떤 HTML에도 편집이 동작합니다.
56
+
57
+ - **클릭 선택** — 호버 시 강조, 클릭 시 선택
58
+ - **드래그 이동** — 선택 요소를 드래그해 이동(CSS transform 사용 → 비파괴적, 레이아웃/반응형 유지)
59
+ - **정렬 스냅 가이드** — 이동 중 다른 요소의 가장자리·중심, 컨테이너 중앙에 스냅 + 빨간 가이드선 (Figma처럼)
60
+ - **리사이즈 핸들** — 모서리를 끌어 크기 조절. **이미지는 `%`로 리사이즈** → 화면에 따라 스케일(모바일에서 작게, 데스크톱에서 크게)
61
+ - **영역 선택(마퀴)** — 박스로 완전히 감싼 요소를 선택(예측 가능: 최외곽 항목, 여러 자식을 가진 래퍼는 자식들로 내려감)
62
+ - **그룹/해제** — 2개 이상 선택 후 Group → 공유 id 부여(래퍼 없음 → 레이아웃 안 깨짐)로 함께 이동. 멤버 클릭 시 그룹 선택 → Ungroup
63
+ - **텍스트 인라인 편집** — 텍스트 더블클릭 → 떠 있는 툴바로 **굵게 / 기울임 / 밑줄 / 링크**
64
+ - **스타일 인스펙터** — 색상·배경·폰트 크기/굵기·줄간격·자간·패딩·모서리·너비, 그리고 **배경 그라디언트·배경 이미지**(업로드 또는 URL)
65
+ - **이미지 교체** — 이미지 선택 → 파일 **Upload**(data URI로 임베드) 또는 **URL** 붙여넣기
66
+ - **블록 추가** — 제목·텍스트·버튼·이미지·구분선 삽입
67
+ - **섹션 템플릿** — 완성된 **Hero / Features / CTA** 섹션 삽입(자체 스타일·반응형)
68
+ - **구조 편집** — 선택 요소 복제·삭제·순서 변경(위/아래)
69
+ - **반응형 프리뷰** — **Desktop / Tablet / Mobile** 폭 전환, 미디어쿼리가 실제 기기처럼 반응
70
+ - **코드 뷰** — **Code**로 전환해 raw HTML 직접 편집 → **Design**으로 돌아오면 즉시 반영. viewport meta 자동 주입으로 export 시 반응형
71
+ - **선택 → 에이전트** — `onEditElement`를 넘기면 "이 선택에 지시 적용" 바가 떠서, 고른 요소를 에이전트에게 바꿔달라고 요청 가능
72
+
73
+ ### 📊 스프레드시트 (`table`) — 진짜 스프레드시트
74
+
75
+ 정적 표가 아니라 스프레드시트 엔진(Fortune-sheet) 위에서 동작합니다.
76
+
77
+ - **실시간 수식** — `=SUM(C2:C4)`, `=AVERAGE(...)`, `=A2*B2` 입력 시 계산. 셀참조·범위·함수 자동완성 지원
78
+ - **데이터로 온 수식** — 에이전트가 값으로 보낸 수식(`"=AVERAGE(B2:B4)"`)은 **로드 시 미리 계산**되어 결과가 바로 표시
79
+ - **전체 툴바** — 폰트, 숫자/통화/% 서식, 굵게/기울임, 테두리, 셀 병합, 정렬, 다중 시트 — 데스크톱 수준
80
+ - 넓은 그리드에서 **상하좌우 부드러운 스크롤**
81
+ - **Export** — `.xlsx`(폰트/병합/서식 포함) 또는 `.csv`
82
+
83
+ ### 🖼️ 슬라이드 (`slides`) — 프리 캔버스 덱
84
+
85
+ 모든 요소가 움직이는 PowerPoint 스타일 편집기.
86
+
87
+ - **자유 배치** — 텍스트/이미지 요소 드래그·리사이즈, 가이드 스냅
88
+ - **인라인 편집** — 더블클릭 텍스트 편집, 굵게/크기/색/정렬 툴바
89
+ - **구조** — 슬라이드 추가/복제/삭제/순서, 썸네일 레일, 발표자 노트
90
+ - **테마·배경**, 발표 모드(전체화면, 방향키 이동)
91
+ - **Export** — `.pptx`, **Figma 복사**(에디터블 프레임으로 바로 붙여넣기), **PDF**(전 슬라이드)
92
+
93
+ ### 📝 문서 (`document`) — Markdown / Word
94
+
95
+ - **클릭 편집** — 페이지를 Markdown으로 편집(GFM 렌더)
96
+ - **Export** — `.docx`, `.md`, `.pdf`, `.html`
97
+
98
+ ### 📈 차트 (`chart`)
99
+
100
+ - **선/막대/영역/파이**, 한 번에 전환
101
+ - **데이터 인라인 편집** — 값 수정, 행 추가/삭제
102
+ - 시리즈(또는 파이 조각)별 **색상 변경**, 시리즈 이름, Y축 라벨, 스택 토글
103
+ - **Export** — `.pdf`(차트가 SVG라 선명) 또는 raw JSON
104
+
105
+ ### 📁 파일 — 라운드트립
106
+
107
+ - **Import** — 드래그앤드롭 또는 파일 선택: **CSV · Excel · Markdown · HTML · JSON** → 편집 가능한 아티팩트로
108
+ - **Export** — 각 아티팩트를 네이티브 포맷으로, 그리고 범용 **standalone `.html`** · **PDF**(브라우저 인쇄)
109
+
110
+ ### 🧰 모든 아티팩트 공통
111
+
112
+ - **실행취소/재실행** — `⌘Z` / `⌘⇧Z`(또는 툴바 버튼)로 *사용자* 편집을 되돌림(에이전트 스트리밍은 스택 오염 안 됨)
113
+ - **버전 히스토리** — `canvas.replace`마다 버전 스냅샷을 남겨 되짚어 보기 가능
114
+ - **에러 격리** — 렌더러가 throw해도 호스트 앱이 죽지 않고 인라인 폴백 표시
115
+ - **다중 캔버스** — `<CanvasProvider>`로 감싸 한 앱에 독립 인스턴스 여러 개
116
+
117
+ ---
118
+
119
+ ## 내 앱에 래핑하기
120
+
121
+ - **peerDependency:** React 18 또는 19 — 직접 제공. ESM 전용.
122
+ - **스타일:** `import "@braincrew-lab/langchain-canvas/styles.css"` 한 번.
123
+ - **인스턴스 격리:** `<CanvasProvider>`가 서브트리마다 독립 store 제공.
124
+ - **커스텀 렌더러:** `registry`로 타입 렌더 방식 추가/오버라이드.
125
+
126
+ ```tsx
127
+ import { Canvas, mergeRegistries, builtinRenderers } from "@braincrew-lab/langchain-canvas";
128
+
129
+ const registry = mergeRegistries(builtinRenderers, {
130
+ metric: ({ artifact }) => <div className="big-number">{artifact.data.value}</div>,
131
+ });
132
+
133
+ <Canvas registry={registry} />
134
+ ```
135
+
136
+ ### 백엔드 없이 — 픽스처 재생 / mock
137
+
138
+ ```tsx
139
+ import { useCanvasReplay, scenarios } from "@braincrew-lab/langchain-canvas";
140
+
141
+ const { play } = useCanvasReplay();
142
+ useEffect(() => { play(scenarios.find((s) => s.id === "table")!.events); }, [play]);
143
+ ```
144
+
145
+ ```tsx
146
+ useCanvasStream({ mock: (msg) => (/차트|chart/i.test(msg) ? chartEvents : null) }); // null → 엔드포인트로
147
+ ```
148
+
149
+ ## 동작 방식
150
+
151
+ ```
152
+ 에이전트 ──SSE──▶ canvas 이벤트 ──▶ reconciler ──▶ store ──▶ 렌더러
153
+ (create/append/patch/replace) (순수 함수) │
154
+
155
+ 사용자 편집 (입력 / 드래그 / 선택) ──▶ 같은 reconciler
156
+ ```
157
+
158
+ reconciler는 단일 순수 함수 — 스트리밍 토큰, 사용자 편집, 새 버전 모두 여기를 거쳐 상태가 예측 가능하고 감사 가능합니다.
159
+
160
+ ## 보안
161
+
162
+ 에이전트 출력과 import된 파일은 신뢰불가로 취급합니다:
163
+
164
+ - HTML은 `sandbox="allow-scripts"` + **`allow-same-origin` 없음**인 iframe에서 렌더 — null 오리진이라 앱의 DOM·쿠키·스토리지에 접근 불가(Claude Artifacts 모델)
165
+ - PDF export는 **스크립트 비활성** 샌드박스 iframe에서 렌더 — 악성 페이지를 내보내도 내 오리진에서 아무것도 실행되지 않음
166
+ - import된 Markdown은 raw HTML 통과 없이 렌더
167
+
168
+ ## 라이선스
169
+
170
+ MIT
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @braincrew-lab/langchain-canvas
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.
4
+
5
+ Think ChatGPT Canvas or Claude Artifacts, but a package you drop into your own React app and point at your own agent.
6
+
7
+ > [한국어 README](./README.ko.md)
8
+
9
+ ```tsx
10
+ const { sendMessage, messages, canvas } = useCanvasStream({ endpoint: "/api/chat" });
11
+ // user: "make a pricing table for 3 plans"
12
+ // → a real spreadsheet appears in <Canvas />, formulas and all
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm i @braincrew-lab/langchain-canvas
19
+ ```
20
+
21
+ Export/import for Office formats uses optional peers — install only what you need:
22
+
23
+ ```bash
24
+ npm i exceljs docx pptxgenjs fast-formula-parser
25
+ ```
26
+
27
+ If a format's package isn't installed, that one export just tells the user what to add; everything else keeps working.
28
+
29
+ ## Mount it
30
+
31
+ Two things: the hook that receives your agent's stream, and the panel that renders it. They share a store, so nothing is wired between them.
32
+
33
+ ```tsx
34
+ import { Canvas, useCanvasStream } from "@braincrew-lab/langchain-canvas";
35
+ import "@braincrew-lab/langchain-canvas/styles.css";
36
+
37
+ function App() {
38
+ const { sendMessage, messages } = useCanvasStream({ endpoint: "/api/chat" });
39
+ return <Canvas />; // render sendMessage/messages in your own chat UI
40
+ }
41
+ ```
42
+
43
+ - `useCanvasStream({ endpoint })` → `{ sendMessage, messages, canvas, isStreaming, editSelection }`.
44
+ - `<Canvas />` ships with `"use client"`, so it drops into a **Next.js App Router** file as-is.
45
+ - `/api/chat` streams SSE using the wire protocol (`canvas.create` / `append` / `patch` / `replace`). The companion Python package emits these from a LangChain/LangGraph agent; any backend sending the same JSON frames works.
46
+
47
+ That's the whole setup. The rest of this README is **how to use each feature** — all of it is direct manipulation the user does on the rendered artifact, with no extra code from you.
48
+
49
+ ---
50
+
51
+ ## Features & how to use them
52
+
53
+ ### 🌐 Web pages (`html`) — a visual page builder
54
+
55
+ HTML is the base substrate, rendered in a sandboxed iframe. Selecting and editing works on any HTML the agent produces.
56
+
57
+ - **Click to select** — hover highlights an element, click selects it.
58
+ - **Drag to move** — drag a selected element to reposition it (uses a CSS transform, so it's non-destructive and keeps the page's flow/responsiveness).
59
+ - **Alignment snap guides** — while moving, edges/centers snap to nearby elements and the container center, with red guide lines (like Figma).
60
+ - **Resize handle** — drag the corner to resize. **Images resize in `%`**, so they scale with the layout — smaller on mobile, larger on desktop.
61
+ - **Marquee select** — drag a box to select everything fully inside it; the selection is predictable (outermost items, drilling into a wrapper of several children).
62
+ - **Group / ungroup** — select 2+ elements and Group; they get a shared id (no wrapper, so layout never breaks) and move together. Click a member to select the group, then Ungroup.
63
+ - **Edit text inline** — double-click any text; a floating toolbar gives **bold / italic / underline / link**.
64
+ - **Style inspector** — color, background, font size/weight, line-height, letter-spacing, padding, radius, width, plus **background gradients and background images** (upload or URL).
65
+ - **Replace images** — select an image → **Upload** a file (embedded as a data URI) or paste a **URL**.
66
+ - **Add blocks** — insert a heading, text, button, image, or divider from the toolbar.
67
+ - **Section templates** — drop in a ready-made **Hero / Features / Call-to-action** section (self-styled and responsive).
68
+ - **Structural actions** — duplicate, delete, reorder (move up/down) the selected element.
69
+ - **Responsive preview** — toggle **Desktop / Tablet / Mobile** width; media queries respond as they would on a real device.
70
+ - **Code view** — switch to **Code** to edit the raw HTML by hand; switch back to **Design** and the change is live. A viewport meta is injected automatically so the page is responsive on export.
71
+ - **Selection → agent** — pass `onEditElement` and a "apply an instruction to this selection" bar appears, so the user can ask the agent to change exactly what they picked.
72
+
73
+ ### 📊 Spreadsheets (`table`) — a real spreadsheet
74
+
75
+ Runs on a spreadsheet engine (Fortune-sheet), not a static table.
76
+
77
+ - **Live formulas** — type `=SUM(C2:C4)`, `=AVERAGE(...)`, `=A2*B2`; they calculate, with cell references, ranges, and function autocomplete.
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.
79
+ - **Full toolbar** — fonts, number/currency/percent formats, bold/italic, borders, cell merging, alignment, multiple sheets — like a desktop spreadsheet.
80
+ - **Smooth scrolling** in both directions over a large grid.
81
+ - **Export** to `.xlsx` (with fonts/merges/formats) or `.csv`.
82
+
83
+ ### 🖼️ Slides (`slides`) — a free-canvas deck
84
+
85
+ A PowerPoint-style editor where every element is movable.
86
+
87
+ - **Free positioning** — drag and resize text/image elements; snap to guides.
88
+ - **Inline editing** — double-click to edit text; format toolbar for bold/size/color/align.
89
+ - **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).
92
+
93
+ ### 📝 Documents (`document`) — Markdown / Word
94
+
95
+ - **Click-to-edit** the page as Markdown, rendered with GFM.
96
+ - **Export** to `.docx`, `.md`, `.pdf`, or `.html`.
97
+
98
+ ### 📈 Charts (`chart`)
99
+
100
+ - **Line / bar / area / pie**, switchable in one click.
101
+ - **Edit data inline** — a small grid to change values, add/remove rows.
102
+ - **Recolor** each series (or each pie slice), rename series, set the y-axis label, toggle stacking.
103
+ - **Export** to `.pdf` (the chart is SVG, so it prints crisply) or the raw JSON.
104
+
105
+ ### 📁 Files — round-trip
106
+
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).
109
+
110
+ ### 🧰 Across every artifact
111
+
112
+ - **Undo / redo** — `⌘Z` / `⌘⇧Z` (or the toolbar buttons) revert *user* edits (agent streaming isn't polluted into the stack).
113
+ - **Version history** — each `canvas.replace` snapshots a version you can step back through.
114
+ - **Error isolation** — a renderer that throws shows an inline fallback instead of crashing the host app.
115
+ - **Multiple canvases** — wrap trees in `<CanvasProvider>` to run independent instances in one app.
116
+
117
+ ---
118
+
119
+ ## Wrapping it in your app
120
+
121
+ - **Peer dependency:** React 18 or 19 — you bring your own. ESM only.
122
+ - **Styles:** `import "@braincrew-lab/langchain-canvas/styles.css"` once.
123
+ - **Isolated instances:** `<CanvasProvider>` gives each subtree its own store.
124
+ - **Bring your own renderer:** pass `registry` to add or override how a type renders.
125
+
126
+ ```tsx
127
+ import { Canvas, mergeRegistries, builtinRenderers } from "@braincrew-lab/langchain-canvas";
128
+
129
+ const registry = mergeRegistries(builtinRenderers, {
130
+ metric: ({ artifact }) => <div className="big-number">{artifact.data.value}</div>,
131
+ });
132
+
133
+ <Canvas registry={registry} />
134
+ ```
135
+
136
+ ### No backend? Replay a fixture or mock the chat
137
+
138
+ ```tsx
139
+ import { useCanvasReplay, scenarios } from "@braincrew-lab/langchain-canvas";
140
+
141
+ const { play } = useCanvasReplay();
142
+ useEffect(() => { play(scenarios.find((s) => s.id === "table")!.events); }, [play]);
143
+ ```
144
+
145
+ ```tsx
146
+ useCanvasStream({ mock: (msg) => (/chart/i.test(msg) ? chartEvents : null) }); // null → hit the endpoint
147
+ ```
148
+
149
+ ## How it works
150
+
151
+ ```
152
+ agent ──SSE──▶ canvas events ──▶ reconciler ──▶ store ──▶ renderer
153
+ (create/append/patch/replace) (pure function) │
154
+
155
+ user edits (type / drag / select) ──▶ same reconciler
156
+ ```
157
+
158
+ The reconciler is a single pure function — every change (a streamed token, a user edit, a new version) goes through it, so state stays predictable and auditable. Renderers only read the reconciled artifact.
159
+
160
+ ## Security
161
+
162
+ Agent output and imported files are treated as untrusted:
163
+
164
+ - HTML renders in an iframe with `sandbox="allow-scripts"` and **no** `allow-same-origin` — a null origin with no reach into your app's DOM, cookies, or storage (the Claude Artifacts model).
165
+ - PDF export renders in a **script-disabled** sandboxed iframe, so exporting a malicious page can't run anything in your origin.
166
+ - Imported Markdown renders without raw-HTML passthrough.
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,182 @@
1
+ import { useArtifactPatch } from './chunk-HHJWIO2C.js';
2
+ import './chunk-PSIT32I5.js';
3
+ import { useState, useMemo, useRef, useEffect } from 'react';
4
+ import * as echarts from 'echarts/core';
5
+ import { BarChart, LineChart, PieChart } from 'echarts/charts';
6
+ import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
7
+ import { CanvasRenderer } from 'echarts/renderers';
8
+ import { jsx, jsxs } from 'react/jsx-runtime';
9
+
10
+ echarts.use([BarChart, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);
11
+ var PALETTE = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#0ea5e9", "#a855f7"];
12
+ var seriesColor = (series, index) => series.color ?? PALETTE[index % PALETTE.length];
13
+ var sliceColor = (options, index) => options?.colors?.[index] ?? PALETTE[index % PALETTE.length];
14
+ var CHART_TYPES = ["bar", "line", "area", "pie"];
15
+ function EChart({ option, height }) {
16
+ const boxRef = useRef(null);
17
+ const instRef = useRef(null);
18
+ useEffect(() => {
19
+ if (!boxRef.current) return;
20
+ const inst = echarts.init(boxRef.current, void 0, { renderer: "canvas" });
21
+ instRef.current = inst;
22
+ const ro = new ResizeObserver(() => inst.resize());
23
+ ro.observe(boxRef.current);
24
+ return () => {
25
+ ro.disconnect();
26
+ inst.dispose();
27
+ instRef.current = null;
28
+ };
29
+ }, []);
30
+ useEffect(() => {
31
+ instRef.current?.setOption(option, true);
32
+ }, [option]);
33
+ return /* @__PURE__ */ jsx("div", { ref: boxRef, className: "cv-chart__canvas", style: { width: "100%", height } });
34
+ }
35
+ function ChartRenderer({ artifact }) {
36
+ const { chart, rows, xKey, series, options, echartsOption } = artifact.data;
37
+ const patch = useArtifactPatch(artifact.id);
38
+ const [editing, setEditing] = useState(false);
39
+ const option = useMemo(
40
+ () => echartsOption ?? toEChartsOption(chart, rows, xKey, series, options),
41
+ [echartsOption, chart, rows, xKey, series, options]
42
+ );
43
+ if (echartsOption) {
44
+ return /* @__PURE__ */ jsx("div", { className: "cv-chart", children: /* @__PURE__ */ jsx(EChart, { option, height: 340 }) });
45
+ }
46
+ if (rows.length === 0) {
47
+ return /* @__PURE__ */ jsx("div", { className: "cv-chart cv-chart--empty", children: "Waiting for data\u2026" });
48
+ }
49
+ const isPie = chart === "pie";
50
+ const setSeries = (i, partial) => patch({ series: series.map((s, j) => j === i ? { ...s, ...partial } : s) });
51
+ const setSliceColor = (i, color) => {
52
+ const colors = rows.map((_, j) => options?.colors?.[j] ?? PALETTE[j % PALETTE.length]);
53
+ colors[i] = color;
54
+ patch({ options: { ...options, colors } });
55
+ };
56
+ const setCell = (rowIdx, key, value) => patch({ rows: rows.map((r, j) => j === rowIdx ? { ...r, [key]: value } : r) });
57
+ const addRow = () => {
58
+ const blank = { [xKey]: `Item ${rows.length + 1}` };
59
+ series.forEach((s) => blank[s.key] = 0);
60
+ patch({ rows: [...rows, blank] });
61
+ };
62
+ const removeRow = (i) => patch({ rows: rows.filter((_, j) => j !== i) });
63
+ return /* @__PURE__ */ jsxs("div", { className: "cv-chart", children: [
64
+ /* @__PURE__ */ jsxs("div", { className: "cv-chart__toolbar cv-chrome", children: [
65
+ /* @__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)) }),
66
+ (chart === "bar" || chart === "area") && /* @__PURE__ */ jsxs("label", { className: "cv-chart__stack", children: [
67
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: !!options?.stacked, onChange: (e) => patch({ options: { ...options, stacked: e.target.checked } }) }),
68
+ "Stacked"
69
+ ] }),
70
+ /* @__PURE__ */ jsx("span", { className: "cv-chart__spacer" }),
71
+ /* @__PURE__ */ jsx("button", { className: `cv-edit-btn ${editing ? "is-primary" : ""}`, onClick: () => setEditing((v) => !v), children: editing ? "Done" : "Edit data" })
72
+ ] }),
73
+ editing && /* @__PURE__ */ jsxs("div", { className: "cv-chart__editor cv-chrome", children: [
74
+ /* @__PURE__ */ jsx("div", { className: "cv-chart__legend", children: isPie ? rows.map((r, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
75
+ /* @__PURE__ */ jsx("input", { type: "color", value: sliceColor(options, i), onChange: (e) => setSliceColor(i, e.target.value), title: `Color: ${r[xKey]}` }),
76
+ /* @__PURE__ */ jsx("span", { className: "cv-chart__swatch-label", children: String(r[xKey]) })
77
+ ] }, i)) : series.map((s, i) => /* @__PURE__ */ jsxs("span", { className: "cv-chart__swatch", children: [
78
+ /* @__PURE__ */ jsx("input", { type: "color", value: seriesColor(s, i), onChange: (e) => setSeries(i, { color: e.target.value }), title: "Series color" }),
79
+ /* @__PURE__ */ jsx(
80
+ "input",
81
+ {
82
+ className: "cv-chart__series-name",
83
+ value: s.label ?? s.key,
84
+ onChange: (e) => setSeries(i, { label: e.target.value }),
85
+ title: "Series name"
86
+ }
87
+ )
88
+ ] }, s.key)) }),
89
+ !isPie && /* @__PURE__ */ jsxs("label", { className: "cv-chart__ylabel", children: [
90
+ "Y-axis",
91
+ /* @__PURE__ */ jsx("input", { value: options?.yLabel ?? "", placeholder: "label\u2026", onChange: (e) => patch({ options: { ...options, yLabel: e.target.value } }) })
92
+ ] }),
93
+ /* @__PURE__ */ jsx(DataGrid, { rows, xKey, series, onCell: setCell, onAddRow: addRow, onRemoveRow: removeRow })
94
+ ] }),
95
+ /* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340 })
96
+ ] });
97
+ }
98
+ function DataGrid({
99
+ rows,
100
+ xKey,
101
+ series,
102
+ onCell,
103
+ onAddRow,
104
+ onRemoveRow
105
+ }) {
106
+ return /* @__PURE__ */ jsxs("div", { className: "cv-chart__grid", children: [
107
+ /* @__PURE__ */ jsxs("table", { children: [
108
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
109
+ /* @__PURE__ */ jsx("th", { children: xKey }),
110
+ series.map((s) => /* @__PURE__ */ jsx("th", { children: s.label ?? s.key }, s.key)),
111
+ /* @__PURE__ */ jsx("th", { "aria-label": "Remove" })
112
+ ] }) }),
113
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
114
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { value: String(row[xKey] ?? ""), onChange: (e) => onCell(i, xKey, e.target.value) }) }),
115
+ series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
116
+ "input",
117
+ {
118
+ type: "number",
119
+ value: Number(row[s.key] ?? 0),
120
+ onChange: (e) => onCell(i, s.key, e.target.value === "" ? 0 : Number(e.target.value))
121
+ }
122
+ ) }, s.key)),
123
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("button", { className: "cv-chart__row-del", onClick: () => onRemoveRow(i), disabled: rows.length <= 1, title: "Remove row", children: "\xD7" }) })
124
+ ] }, i)) })
125
+ ] }),
126
+ /* @__PURE__ */ jsx("button", { className: "cv-chart__addrow", onClick: onAddRow, children: "+ Add row" })
127
+ ] });
128
+ }
129
+ function toEChartsOption(chart, rows, xKey, series, options) {
130
+ const AXIS = "#9aa4b2";
131
+ if (chart === "pie") {
132
+ const valueKey = series[0]?.key ?? "value";
133
+ return {
134
+ tooltip: { trigger: "item" },
135
+ legend: { bottom: 0, textStyle: { color: AXIS } },
136
+ series: [
137
+ {
138
+ type: "pie",
139
+ radius: ["45%", "72%"],
140
+ padAngle: 2,
141
+ itemStyle: { borderRadius: 4 },
142
+ label: { color: AXIS },
143
+ data: rows.map((r, i) => ({
144
+ name: String(r[xKey] ?? ""),
145
+ value: Number(r[valueKey] ?? 0),
146
+ itemStyle: { color: sliceColor(options, i) }
147
+ }))
148
+ }
149
+ ]
150
+ };
151
+ }
152
+ const stacked = (chart === "bar" || chart === "area") && !!options?.stacked;
153
+ return {
154
+ tooltip: { trigger: "axis" },
155
+ legend: { bottom: 0, textStyle: { color: AXIS } },
156
+ grid: { left: 8, right: 16, top: 24, bottom: 40, containLabel: true },
157
+ xAxis: {
158
+ type: "category",
159
+ data: rows.map((r) => String(r[xKey] ?? "")),
160
+ axisLabel: { color: AXIS, fontSize: 12 },
161
+ axisLine: { lineStyle: { color: "#d4d7dd" } }
162
+ },
163
+ yAxis: {
164
+ type: "value",
165
+ name: options?.yLabel,
166
+ nameTextStyle: { color: AXIS },
167
+ axisLabel: { color: AXIS, fontSize: 12 },
168
+ splitLine: { lineStyle: { color: "#e5e7eb", type: "dashed" } }
169
+ },
170
+ series: series.map((s, i) => ({
171
+ name: s.label ?? s.key,
172
+ type: chart === "area" ? "line" : chart,
173
+ ...chart === "area" ? { areaStyle: { opacity: 0.2 }, smooth: false } : {},
174
+ ...chart !== "bar" ? { showSymbol: false, lineStyle: { width: 2 } } : {},
175
+ stack: stacked ? "total" : void 0,
176
+ itemStyle: { color: seriesColor(s, i) },
177
+ data: rows.map((r) => Number(r[s.key] ?? 0))
178
+ }))
179
+ };
180
+ }
181
+
182
+ export { ChartRenderer };
@@ -0,0 +1,40 @@
1
+ import { useArtifactPatch } from './chunk-HHJWIO2C.js';
2
+ import './chunk-PSIT32I5.js';
3
+ import { useState } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+
8
+ function DocumentRenderer({ artifact }) {
9
+ const { content } = artifact.data;
10
+ const patch = useArtifactPatch(artifact.id);
11
+ const [draft, setDraft] = useState(null);
12
+ const editing = draft !== null;
13
+ const commit = () => {
14
+ if (draft !== null && draft !== content) patch({ content: draft });
15
+ setDraft(null);
16
+ };
17
+ return /* @__PURE__ */ jsx("div", { className: "cv-word", children: /* @__PURE__ */ jsx("div", { className: "cv-word__page", children: editing ? /* @__PURE__ */ jsx(
18
+ "textarea",
19
+ {
20
+ className: "cv-doc-editor",
21
+ value: draft,
22
+ autoFocus: true,
23
+ onBlur: commit,
24
+ onChange: (e) => setDraft(e.target.value)
25
+ }
26
+ ) : /* @__PURE__ */ jsxs(
27
+ "article",
28
+ {
29
+ className: "cv-doc",
30
+ title: "Click to edit",
31
+ onClick: () => artifact.status !== "streaming" && setDraft(content),
32
+ children: [
33
+ /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }),
34
+ artifact.status === "streaming" && /* @__PURE__ */ jsx("span", { className: "cv-caret", "aria-hidden": true })
35
+ ]
36
+ }
37
+ ) }) });
38
+ }
39
+
40
+ export { DocumentRenderer };