Skip to content

结构化数据

通过 v-model:data 获取所有已解析区块的结构化信息。

vue
<StreamContains v-model:data="blocks" :model-value="text" mode="accurate">
  <Think />
</StreamContains>

表单联动示例

配合内置表单组件(InputBlockSelectBlockButtonBlock),可以实时捕获用户交互数据并反映在 blocks 中。


blocks (0)
[]
form state
{
  "values": {},
  "valid": true,
  "errors": {},
  "submitted": false
}
<template>
  <div class="s-form-demo">
    <Input v-model="text" :rows="5" :simulate-text="exampleText" />

    <hr>

    <div class="s-form-demo-render">
      <StreamContains v-model:data="blocks" :model-value="text" mode="accurate">
        <InputBlock />
        <SelectBlock />
        <ButtonBlock />
      </StreamContains>
    </div>

    <details class="s-form-demo-blocks" open>
      <summary>blocks ({{ blocks.length }})</summary>
      <pre>{{ JSON.stringify(blocks, null, 2) }}</pre>
    </details>

    <details class="s-form-demo-blocks" open>
      <summary>form state</summary>
      <pre>{{ JSON.stringify({
        values: form.values,
        valid: form.valid,
        errors: form.errors,
        submitted: form.submitted,
        lastAction: form.lastAction
      }, null, 2) }}</pre>
    </details>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { StreamContains, InputBlock, SelectBlock, ButtonBlock, useStreamFormData } from 'stream-ui'
import Input from '../../.comm/Input.vue'

const exampleText = [
  '<input-block id="name-input" name="name" label="姓名" placeholder="请输入姓名" required />',
  '<select-block id="department-select" name="department" label="部门" options="技术部,市场部,财务部,人事部" required />',
  '<button-block id="submit-application" action="submit" label="提交申请" />'
].join('\n')
const text = ref(exampleText)
const blocks = ref([])
const form = useStreamFormData(blocks)
</script>

<style scoped>
.s-form-demo-render {
  padding: 8px 0;
}

.s-form-demo-blocks {
  margin-top: 12px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 13px;
}

.s-form-demo-blocks summary {
  cursor: pointer;
  font-weight: 600;
  color: #475569;
}

.s-form-demo-blocks pre {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
</style>

StreamBlockData

ts
interface StreamBlockData {
  id: string                                    // 区块唯一 ID
  category: 'component' | 'fallback' | 'text'   // 区块分类
  tagName: string                                // 标签名(小写)
  attrs?: Record<string, string | boolean>       // 标签属性
  content: string                                // 内部内容
  isClosed: boolean                              // 是否已闭合
  payload?: unknown                              // 子组件回传数据
}

三种 category:

  • component:命中注册组件的标签
  • fallback:未注册标签,由 DefaultTag 渲染
  • text:标签外的普通文本

去抖更新

areBlocksEqual 对比前后区块数组,若完全相同则跳过 emit('update:data')。更新在 queueMicrotask 中批量执行。

reportData

子组件调用 props.reportData(value) 后,对应区块的 payload 字段会更新,触发 v-model:data 更新。

vue
<script setup>
const props = defineProps(['reportData'])
const onChange = (e) => {
  props.reportData({ value: e.target.value })
}
</script>

表单数据归一化

对于内置的 InputBlockSelectBlockButtonBlock,可以使用 useStreamFormData(blocks) 将原始区块数组整理成表单视图。非 Vue 场景也可以直接调用 collectStreamFormData(blocks)

ts
import { ref } from 'vue'
import { useStreamFormData } from 'stream-ui'

const blocks = ref([])
const form = useStreamFormData(blocks)

console.log(form.value.values.email)

form 同时提供提交和校验状态,适合直接接入业务提交逻辑:

ts
if (form.value.submitted && form.value.valid) {
  submit(form.value.values)
}

if (!form.value.valid) {
  console.log(form.value.errors)
}

建议让 LLM 为交互字段生成稳定的 id 和业务字段名 name

html
<input-block id="email-input" name="email" label="邮箱" placeholder="请输入邮箱" required />
<select-block id="department-select" name="department" label="部门" options="技术部,市场部,财务部" required />
<button-block id="submit-button" action="submit" label="提交" />

id 用于跨流式更新保留区块 payload,name 用于生成 form.values 的字段键。没有 name 时,collectStreamFormData 会回退使用区块 id。当 action="submit" 的按钮被点击后,form.submitted 会变为 true

初始载荷持久化

initBlockPayloadMap 从外部 props.data 中读取已有 payload,跨渲染周期保留。

标签上的字符串 id 属性会优先作为 StreamBlockData.id。这能让交互组件在流式内容前面插入新文本或新标签后,依旧按稳定 ID 找回已有 payload。

html
<input-block id="email" label="邮箱" placeholder="请输入邮箱" />

如果没有提供字符串 id,Stream UI 会继续使用自动生成的区块 ID。文本区块也始终使用自动 ID。

自闭合标签

<br /><input name="email" /> 等自闭合标签:

  • isClosed 立即为 true
  • 不包含内容
  • 不影响后续文本的解析

Released under the MIT License.