前言
目前各厂的大模型几乎都支持了markdown输出,虽然语法简单,但是提供给用户的UI和交互得丰富。产品经理们也各显神通。举例表格
:UI得好看,交互上支持左右滑动、保存为图片、表格、复制文本、移动端还需要支持横向全屏展示。为了满足产品大大们各种奇思妙想,现在的markdown语法已经扩展了很多,支持了更多复杂的数据与交互。
推荐一个markdown文本解析器markedjs,有机会的话可以再单独聊聊它的使用
将markdown文本通过解析器后输出富文本,再前端渲染。例如:
1. **加粗** => <strong>加粗</strong>
2. # 标题1 => <h1>标题1</h1>
复杂一点的表格语法:
| 表头1 | 表头2 |
| ---- | ---- |
| 内容1 | 内容2 |
对应的富文本如下:
<table>
<thead>
<tr>
<th>表头1</th>
<th>表头2</th>
</tr>
</thead>
<tbody>
<tr>
<td>内容1</td>
<td>内容2</td>
</tr>
</tbody>
</table>
如何添加交互
在一个实际业务场景中,大模型答案流式输出,通过解析器处理成富文本后,前端再innerHTML渲染。
例如:如何给markdown table
添加以下交互?
- 展示表格标题
- 展示表格生成时间
- 与表格相关的功能按钮:点击按钮可以保存表格为图片、下载表格为excel文档、全屏展示
在table渲染时为它插入/拼接上DOM片段、绑定事件、渲染的时间还需要靠JS先计算出来。此时想到一个由来已久却很少使用的Web API:Web Component。
例如原生video标签,开发者只需要设置src,就可以拥有播放、暂停、进度条等UI与交互功能,这些功能被封装在了video组件内部,在控制台-审查元素中无法看到组件内部结构,需要额外开启选项才可查看。
控制台设置里面开启shadow dom
选项。
审查元素:可以看到多了#shadow-root
节点,其下面的就是封装在video内部的UI。
对于开发者,一个自定义Web组件,在组件内实现UI与交互,即独立又可复用。Web组件原生JS即可支持,不依赖React、Vue等前端框架。一个大模型要对接多个平台输出展示,各个平台间也可复用一个Web组件避免重复开发,实现UI与交互的统一。
自定义my-table
组件
1.提供html模板,实现DOM结构与样式,结合slot方便复用。
<template id='my-table'>
<style>
.wrapper {
border: 1px solid #eee;
border-radius: 4px;
}
.table-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #f8f8f8;
}
.content {
overflow: auto;
}
</style>
<div class="wrapper">
<div class="table-title">
<span><slot name="title" class="title">默认标题</slot> (<span class="time"></span>)</span>
<span class="icon" hidden>🔍</span>
</div>
<div class="content"><slot></slot></div>
</div>
</template>
<my-table>
<span slot="title">表格</span>
<table>
<thead>
<tr>
<th>表头1</th>
<th>表头2</th>
<th>表头3</th>
</tr>
</thead>
<tbody>
<tr>
<td>内容1</td>
<td>内容2</td>
<td>内容3</td>
</tr>
</tbody>
</table>
</my-table>
2.定义my-table
组件
class MyTable extends HTMLElement {
constructor(){
super();
}
// 元素添加到文档时
connectedCallback(){
const template = document.getElementById('my-table');
// 通过cloneNode复制一份模板,避免使用源模板
const content = template.content.cloneNode(true);
const shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(content);
}
}
// 注册组件
customElements.define('my-table', MyTable);
3.获取组件的生成时间
某些业务场景下,组件需要二次回显。如果二次回显时再次计算当前渲染的时间是不合理的,因此time需要记录下来,通过
attribute
记录
class MyTable extends HTMLElement {
constructor(){
super();
}
+ get time(){
+ const time = this.getAttribute('time');
+ if(time){
+ return time;
+ }
+ // 时间格式处理
+ const str = new Date().toLocaleTimeString();
+ // 记录时间,二次渲染使用
+ this.setAttribute('time', str);
+ return str;
+ }
// 元素添加到文档时
connectedCallback(){
const template = document.getElementById('my-table');
const content = template.content.cloneNode(true);
+ content.querySelector('.time').textContent = this.time;
const shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(content);
}
}
4.功能按钮事件绑定
class MyTable extends HTMLElement {
constructor(){
super();
}
...
// 元素添加到文档时
connectedCallback(){
const template = document.getElementById('my-table');
const content = template.content.cloneNode(true);
content.querySelector('.time').textContent = this.time;
const shadow = this.attachShadow({mode: 'open'});
+ const icon = shadow.querySelector('.icon');
+ icon.onclick = () => {
+ console.log('点击icon')
+ }
shadow.appendChild(content);
}
}
5.折叠屏适配
例如:功能按钮是全屏展示
,期望当表格水平方向展示不全时展示全屏按钮
,否则隐藏按钮。
constructor(){
super();
// 注意this指向
this.fullscreenHandler = this.fullscreenHandler.bind(this);
}
// 处理全屏按钮显示或隐藏
fullscreenHandler(){
// 当出现横向滚动条时,展示icon,否则隐藏
const wrapper = this.shadowRoot.querySelector('.content');
const icon = this.shadowRoot.querySelector('.icon');
// 有滚动条
if(wrapper.scrollWidth > wrapper.clientWidth){
icon.removeAttribute('hidden');
icon.onclick = () => {
console.log('点击icon')
}
} else {
icon.setAttribute('hidden', true);
icon.onclick = null;
}
}
connectedCallback(){
...
- const icon = shadow.querySelector('.icon');
- icon.onclick = () => {
- console.log('点击icon')
- }
+ this.fullscreenHandler();
}
由于现在很多移动设备支持折叠屏了,因此在实现这种功能时要考虑到折叠屏情况。通过监听resize
事件,再次计算是否展示全屏按钮
。
// 元素添加到文档时
connectedCallback(){
...
this.fullscreenHandler();
// 适配折叠屏切换时
window.addEventListener('resize', this.fullscreenHandler);
}
// 元素从文档中移除时
disconnectedCallback(){
window.removeEventListener('resize', this.fullscreenHandler);
}
6.使用my-table
组件
在大模型答案流式输出中,实时通过markedjs
解析器输出富文本,在输出表格时实时替换,将原表格包裹在自定义组件内。浏览器渲染这段富文本时,就可以直接看到我们添加的UI与交互。
'这是一段大模型答案<table>...</table>'
=>
'这是一段大模型答案<my-table><table>...</table></my-table>'
猛戳下方查看完整代码:
最后
Web Component浏览器天然支持,只不过接口是后面版本才开放的,在使用时也要关注兼容性问题。