@jetecho/dsh-csv-and-image-preview 0.1.1 → 0.1.2

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/lib/client.js CHANGED
@@ -1,190 +1,190 @@
1
- /**
2
- * dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
3
- *
4
- * 为两个工具注册键控 toolview(`tool.call.toolview`, key = 工具名):
5
- * - `preview_image`:从结果节点 meta 读取 { src, mime, label },渲染浏览器
6
- * 原生 <img>;
7
- * - `preview_csv`:从结果节点 meta 读取 { label, delimiter, rows, ... },
8
- * 渲染原生 <table>(粘性表头、控件限高 320px 局部滚动、表头点击排序)。
9
- *
10
- * 这样图片/表格都不经过聊天渲染器的 markdown/genui 白名单,直接以真实 DOM
11
- * 显示。样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
12
- * @module dsh-csv-and-image-preview/client
13
- */
14
-
15
- window.__ModuleLoader__.load({
16
- id: 'dsh-csv-and-image-preview',
17
- factory: (require) => {
18
- var module = { exports: {} }
19
- var exports = module.exports
20
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
21
-
22
- const React = require('react')
23
-
24
- // ── 样式 ────────────────────────────────────────────────────────────────
25
- const css = [
26
- '/* dsh-csv-and-image-preview */',
27
- '.dip-root{display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%}',
28
- '.dip-label{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:20px}',
29
- '.dip-figure{margin:0;padding:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);max-width:100%}',
30
- '.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
31
- '.dip-img.svg{background:#fff}',
32
- '.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
33
- '/* preview_csv 表格:控件限高,内部滚动,不撑爆聊天卡片 */',
34
- '.dip-table-wrap{max-height:320px;max-width:100%;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}',
35
- '.dip-table{border-collapse:separate;border-spacing:0;font-size:12px;line-height:20px;white-space:nowrap}',
36
- '.dip-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-weight:600;text-align:left;cursor:pointer;user-select:none;-webkit-user-select:none}',
37
- '.dip-table th,.dip-table td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);overflow:hidden;text-overflow:ellipsis}',
38
- '.dip-table th:last-child,.dip-table td:last-child{border-right:none}',
39
- '.dip-table tbody tr:nth-child(even) td{background:rgba(127,127,127,0.08)}',
40
- '.dip-table td.num{text-align:right;font-variant-numeric:tabular-nums}',
41
- '.dip-csv-note{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}',
42
- ].join('\n')
43
- if (typeof document !== 'undefined') {
44
- const key = 'dip-' + 'dsh-csv-and-image-preview'
45
- if (document.querySelector('style[data-plugin-css="' + key + '"]') === null) {
46
- const el = document.createElement('style')
47
- el.dataset.plugin = 'dsh-csv-and-image-preview'
48
- el.dataset.pluginCss = key
49
- el.textContent = css
50
- document.head.appendChild(el)
51
- }
52
- }
53
-
54
- /** 从 props.block 取 meta;工具调用 running 或回退时 meta 不存在。 */
55
- function metaOf(props) {
56
- const block = props && props.block
57
- return block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
58
- }
59
-
60
- /**
61
- * 键控 toolview(preview_image):从 meta 读取 { src, mime, label } 渲染
62
- * 原生 <img>。
63
- */
64
- function PreviewToolView(props) {
65
- const meta = metaOf(props)
66
- const label = typeof meta === 'object' && meta !== null && typeof meta.label === 'string' ? meta.label : '预览'
67
- const src = typeof meta === 'object' && meta !== null && typeof meta.src === 'string' ? meta.src : null
68
- const isSvg = typeof meta === 'object' && meta !== null && meta.mime === 'image/svg+xml'
69
- if (src === null) {
70
- return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
71
- React.createElement('span', { className: 'dip-missing' }, '图片预览缺少数据(meta 缺失)'))
72
- }
73
- return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
74
- React.createElement('span', { className: 'dip-label' }, label),
75
- React.createElement('figure', { className: 'dip-figure' },
76
- React.createElement('img', {
77
- className: 'dip-img' + (isSvg ? ' svg' : ''),
78
- src: src,
79
- alt: label,
80
- })))
81
- }
82
-
83
- const NUMERIC_RE = /^-?\d+(\.\d+)?$/
84
-
85
- /** 截断/统计脚注:「显示前 50 / 1234 行 · 前 5 / 8 列 · 分隔符 ","」。 */
86
- function csvNote(meta) {
87
- const parts = []
88
- parts.push(meta.rowsShown < meta.totalRows
89
- ? `显示前 ${meta.rowsShown} / ${meta.totalRows} 行`
90
- : `共 ${meta.totalRows} 行`)
91
- if (meta.colsShown < meta.totalCols) parts.push(`前 ${meta.colsShown} / ${meta.totalCols} 列`)
92
- parts.push(`分隔符 "${meta.delimiter === '\t' ? 'Tab' : meta.delimiter}"`)
93
- return parts.join(' · ')
94
- }
95
-
96
- /**
97
- * 对数据行(不含表头)按列排序:整列(除空串)可解析为数字时按数值比较,
98
- * 否则按中文 locale 比较;空值恒沉底。dir=1 升序,-1 降序。
99
- */
100
- function sortBodyRows(meta, col, dir) {
101
- const body = meta.rows.slice(1)
102
- const numeric = body.every((r) => {
103
- const v = col < r.length ? r[col] : ''
104
- return v === '' || NUMERIC_RE.test(v)
105
- }) && body.some((r) => col < r.length && NUMERIC_RE.test(r[col]))
106
- const cmp = numeric
107
- ? (a, b) => (parseFloat(a) || 0) - (parseFloat(b) || 0)
108
- : (a, b) => String(a).localeCompare(String(b), 'zh-Hans-CN')
109
- return body
110
- .map((_, i) => i)
111
- .sort((x, y) => {
112
- const a = x < body.length && col < body[x].length ? body[x][col] : ''
113
- const b = y < body.length && col < body[y].length ? body[y][col] : ''
114
- if ((a === '') !== (b === '')) return a === '' ? 1 : -1
115
- return cmp(a, b) * dir
116
- })
117
- .map((i) => body[i])
118
- }
119
-
120
- /**
121
- * 键控 toolview(preview_csv):从 meta 读取 { label, delimiter, rows,
122
- * totalRows, totalCols, rowsShown, colsShown },渲染原生 <table>。
123
- * rows[0] 为表头;数字单元格右对齐;表头点击循环排序(升→降→取消)。
124
- */
125
- function CsvToolView(props) {
126
- const meta = metaOf(props)
127
- const ok = meta !== null && typeof meta === 'object' && Array.isArray(meta.rows) && meta.rows.length > 0
128
- // hooks 必须无条件调用,放在提前 return 之前。
129
- const sortPair = React.useState(null) // null | { col, dir }
130
- const sort = sortPair[0]
131
- const setSort = sortPair[1]
132
- if (!ok) {
133
- return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
134
- React.createElement('span', { className: 'dip-missing' }, 'CSV 预览缺少数据(meta 缺失)'))
135
- }
136
- const label = typeof meta.label === 'string' && meta.label !== '' ? meta.label : 'CSV 预览'
137
-
138
- const headerClick = (ci) => {
139
- setSort((prev) => (prev !== null && prev.col === ci ? (prev.dir === 1 ? { col: ci, dir: -1 } : null) : { col: ci, dir: 1 }))
140
- }
141
-
142
- const thead = React.createElement('thead', null,
143
- React.createElement('tr', null, meta.rows[0].map((c, i) => {
144
- const indicator = sort !== null && sort.col === i ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''
145
- return React.createElement('th', {
146
- key: i,
147
- onClick: () => headerClick(i),
148
- title: String(c),
149
- }, String(c) + indicator)
150
- })))
151
- const bodyRows = sort !== null ? sortBodyRows(meta, sort.col, sort.dir) : meta.rows.slice(1)
152
- const tbody = React.createElement('tbody', null, bodyRows.map((r, ri) =>
153
- React.createElement('tr', { key: ri }, r.map((c, ci) => {
154
- const s = String(c)
155
- return React.createElement('td', {
156
- key: ci,
157
- className: NUMERIC_RE.test(s) ? 'num' : undefined,
158
- title: s,
159
- }, s)
160
- }))))
161
- return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
162
- React.createElement('span', { className: 'dip-label' }, label),
163
- React.createElement('div', { className: 'dip-table-wrap' },
164
- React.createElement('table', { className: 'dip-table' }, thead, tbody)),
165
- React.createElement('span', { className: 'dip-csv-note' }, csvNote(meta)))
166
- }
167
-
168
- // ── 服务接入 ─────────────────────────────────────────────────────────────
169
- const inject = []
170
- function apply(ctx) {
171
- const slots = ctx.get('slots')
172
- if (slots === undefined) return
173
- const disposals = [
174
- slots.inject('tool.call.toolview', () => slots.register(
175
- { name: 'tool.call.toolview', key: 'preview_image' },
176
- PreviewToolView,
177
- )),
178
- slots.inject('tool.call.toolview', () => slots.register(
179
- { name: 'tool.call.toolview', key: 'preview_csv' },
180
- CsvToolView,
181
- )),
182
- ]
183
- return () => { for (const dispose of disposals) dispose() }
184
- }
185
-
186
- exports.apply = apply
187
- exports.inject = inject
188
- return module.exports
189
- },
190
- })
1
+ /**
2
+ * dsh-csv-and-image-preview 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载)。
3
+ *
4
+ * 为两个工具注册键控 toolview(`tool.call.toolview`, key = 工具名):
5
+ * - `preview_image`:从结果节点 meta 读取 { src, mime, label },渲染浏览器
6
+ * 原生 <img>;
7
+ * - `preview_csv`:从结果节点 meta 读取 { label, delimiter, rows, ... },
8
+ * 渲染原生 <table>(粘性表头、控件限高 320px 局部滚动、表头点击排序)。
9
+ *
10
+ * 这样图片/表格都不经过聊天渲染器的 markdown/genui 白名单,直接以真实 DOM
11
+ * 显示。样式使用 --dsw-* 主题变量,跟随全局亮/暗主题。
12
+ * @module dsh-csv-and-image-preview/client
13
+ */
14
+
15
+ window.__ModuleLoader__.load({
16
+ id: '@jetecho/dsh-csv-and-image-preview',
17
+ factory: (require) => {
18
+ var module = { exports: {} }
19
+ var exports = module.exports
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
21
+
22
+ const React = require('react')
23
+
24
+ // ── 样式 ────────────────────────────────────────────────────────────────
25
+ const css = [
26
+ '/* dsh-csv-and-image-preview */',
27
+ '.dip-root{display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%}',
28
+ '.dip-label{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:20px}',
29
+ '.dip-figure{margin:0;padding:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);max-width:100%}',
30
+ '.dip-img{display:block;max-width:100%;height:auto;border-radius:8px}',
31
+ '.dip-img.svg{background:#fff}',
32
+ '.dip-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
33
+ '/* preview_csv 表格:控件限高,内部滚动,不撑爆聊天卡片 */',
34
+ '.dip-table-wrap{max-height:320px;max-width:100%;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}',
35
+ '.dip-table{border-collapse:separate;border-spacing:0;font-size:12px;line-height:20px;white-space:nowrap}',
36
+ '.dip-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-weight:600;text-align:left;cursor:pointer;user-select:none;-webkit-user-select:none}',
37
+ '.dip-table th,.dip-table td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);overflow:hidden;text-overflow:ellipsis}',
38
+ '.dip-table th:last-child,.dip-table td:last-child{border-right:none}',
39
+ '.dip-table tbody tr:nth-child(even) td{background:rgba(127,127,127,0.08)}',
40
+ '.dip-table td.num{text-align:right;font-variant-numeric:tabular-nums}',
41
+ '.dip-csv-note{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}',
42
+ ].join('\n')
43
+ if (typeof document !== 'undefined') {
44
+ const key = 'dip-' + 'dsh-csv-and-image-preview'
45
+ if (document.querySelector('style[data-plugin-css="' + key + '"]') === null) {
46
+ const el = document.createElement('style')
47
+ el.dataset.plugin = 'dsh-csv-and-image-preview'
48
+ el.dataset.pluginCss = key
49
+ el.textContent = css
50
+ document.head.appendChild(el)
51
+ }
52
+ }
53
+
54
+ /** 从 props.block 取 meta;工具调用 running 或回退时 meta 不存在。 */
55
+ function metaOf(props) {
56
+ const block = props && props.block
57
+ return block !== null && typeof block === 'object' && 'meta' in block ? block.meta : undefined
58
+ }
59
+
60
+ /**
61
+ * 键控 toolview(preview_image):从 meta 读取 { src, mime, label } 渲染
62
+ * 原生 <img>。
63
+ */
64
+ function PreviewToolView(props) {
65
+ const meta = metaOf(props)
66
+ const label = typeof meta === 'object' && meta !== null && typeof meta.label === 'string' ? meta.label : '预览'
67
+ const src = typeof meta === 'object' && meta !== null && typeof meta.src === 'string' ? meta.src : null
68
+ const isSvg = typeof meta === 'object' && meta !== null && meta.mime === 'image/svg+xml'
69
+ if (src === null) {
70
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
71
+ React.createElement('span', { className: 'dip-missing' }, '图片预览缺少数据(meta 缺失)'))
72
+ }
73
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
74
+ React.createElement('span', { className: 'dip-label' }, label),
75
+ React.createElement('figure', { className: 'dip-figure' },
76
+ React.createElement('img', {
77
+ className: 'dip-img' + (isSvg ? ' svg' : ''),
78
+ src: src,
79
+ alt: label,
80
+ })))
81
+ }
82
+
83
+ const NUMERIC_RE = /^-?\d+(\.\d+)?$/
84
+
85
+ /** 截断/统计脚注:「显示前 50 / 1234 行 · 前 5 / 8 列 · 分隔符 ","」。 */
86
+ function csvNote(meta) {
87
+ const parts = []
88
+ parts.push(meta.rowsShown < meta.totalRows
89
+ ? `显示前 ${meta.rowsShown} / ${meta.totalRows} 行`
90
+ : `共 ${meta.totalRows} 行`)
91
+ if (meta.colsShown < meta.totalCols) parts.push(`前 ${meta.colsShown} / ${meta.totalCols} 列`)
92
+ parts.push(`分隔符 "${meta.delimiter === '\t' ? 'Tab' : meta.delimiter}"`)
93
+ return parts.join(' · ')
94
+ }
95
+
96
+ /**
97
+ * 对数据行(不含表头)按列排序:整列(除空串)可解析为数字时按数值比较,
98
+ * 否则按中文 locale 比较;空值恒沉底。dir=1 升序,-1 降序。
99
+ */
100
+ function sortBodyRows(meta, col, dir) {
101
+ const body = meta.rows.slice(1)
102
+ const numeric = body.every((r) => {
103
+ const v = col < r.length ? r[col] : ''
104
+ return v === '' || NUMERIC_RE.test(v)
105
+ }) && body.some((r) => col < r.length && NUMERIC_RE.test(r[col]))
106
+ const cmp = numeric
107
+ ? (a, b) => (parseFloat(a) || 0) - (parseFloat(b) || 0)
108
+ : (a, b) => String(a).localeCompare(String(b), 'zh-Hans-CN')
109
+ return body
110
+ .map((_, i) => i)
111
+ .sort((x, y) => {
112
+ const a = x < body.length && col < body[x].length ? body[x][col] : ''
113
+ const b = y < body.length && col < body[y].length ? body[y][col] : ''
114
+ if ((a === '') !== (b === '')) return a === '' ? 1 : -1
115
+ return cmp(a, b) * dir
116
+ })
117
+ .map((i) => body[i])
118
+ }
119
+
120
+ /**
121
+ * 键控 toolview(preview_csv):从 meta 读取 { label, delimiter, rows,
122
+ * totalRows, totalCols, rowsShown, colsShown },渲染原生 <table>。
123
+ * rows[0] 为表头;数字单元格右对齐;表头点击循环排序(升→降→取消)。
124
+ */
125
+ function CsvToolView(props) {
126
+ const meta = metaOf(props)
127
+ const ok = meta !== null && typeof meta === 'object' && Array.isArray(meta.rows) && meta.rows.length > 0
128
+ // hooks 必须无条件调用,放在提前 return 之前。
129
+ const sortPair = React.useState(null) // null | { col, dir }
130
+ const sort = sortPair[0]
131
+ const setSort = sortPair[1]
132
+ if (!ok) {
133
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
134
+ React.createElement('span', { className: 'dip-missing' }, 'CSV 预览缺少数据(meta 缺失)'))
135
+ }
136
+ const label = typeof meta.label === 'string' && meta.label !== '' ? meta.label : 'CSV 预览'
137
+
138
+ const headerClick = (ci) => {
139
+ setSort((prev) => (prev !== null && prev.col === ci ? (prev.dir === 1 ? { col: ci, dir: -1 } : null) : { col: ci, dir: 1 }))
140
+ }
141
+
142
+ const thead = React.createElement('thead', null,
143
+ React.createElement('tr', null, meta.rows[0].map((c, i) => {
144
+ const indicator = sort !== null && sort.col === i ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''
145
+ return React.createElement('th', {
146
+ key: i,
147
+ onClick: () => headerClick(i),
148
+ title: String(c),
149
+ }, String(c) + indicator)
150
+ })))
151
+ const bodyRows = sort !== null ? sortBodyRows(meta, sort.col, sort.dir) : meta.rows.slice(1)
152
+ const tbody = React.createElement('tbody', null, bodyRows.map((r, ri) =>
153
+ React.createElement('tr', { key: ri }, r.map((c, ci) => {
154
+ const s = String(c)
155
+ return React.createElement('td', {
156
+ key: ci,
157
+ className: NUMERIC_RE.test(s) ? 'num' : undefined,
158
+ title: s,
159
+ }, s)
160
+ }))))
161
+ return React.createElement('div', { className: 'dip-root', 'data-dip-preview': true },
162
+ React.createElement('span', { className: 'dip-label' }, label),
163
+ React.createElement('div', { className: 'dip-table-wrap' },
164
+ React.createElement('table', { className: 'dip-table' }, thead, tbody)),
165
+ React.createElement('span', { className: 'dip-csv-note' }, csvNote(meta)))
166
+ }
167
+
168
+ // ── 服务接入 ─────────────────────────────────────────────────────────────
169
+ const inject = []
170
+ function apply(ctx) {
171
+ const slots = ctx.get('slots')
172
+ if (slots === undefined) return
173
+ const disposals = [
174
+ slots.inject('tool.call.toolview', () => slots.register(
175
+ { name: 'tool.call.toolview', key: 'preview_image' },
176
+ PreviewToolView,
177
+ )),
178
+ slots.inject('tool.call.toolview', () => slots.register(
179
+ { name: 'tool.call.toolview', key: 'preview_csv' },
180
+ CsvToolView,
181
+ )),
182
+ ]
183
+ return () => { for (const dispose of disposals) dispose() }
184
+ }
185
+
186
+ exports.apply = apply
187
+ exports.inject = inject
188
+ return module.exports
189
+ },
190
+ })