|
|
@@ -1,152 +1,249 @@
|
|
|
import React, { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
|
+const SCOPES = [
|
|
|
+ ['substance', '实质'],
|
|
|
+ ['form', '形式'],
|
|
|
+ ['feeling', '感受'],
|
|
|
+ ['effect', '作用'],
|
|
|
+ ['intent', '意图'],
|
|
|
+]
|
|
|
+const SC_CN = Object.fromEntries(SCOPES)
|
|
|
+const TYPE_CN = { how: 'How', what: 'What', why: 'Why' }
|
|
|
+const TYPE_LABEL = { how: '怎么做', what: '是什么', why: '为什么' }
|
|
|
+
|
|
|
+const STEPS = [
|
|
|
+ ['读懂帖子', '把图、视频读成文字'],
|
|
|
+ ['判断是否是创作知识', '不是就整帖跳过'],
|
|
|
+ ['拆出知识', '拆成 How / What / Why'],
|
|
|
+ ['校验 how 的严谨性', '假工序拆回 What / Why'],
|
|
|
+ ['给知识归类', '标作用域并定位分类树'],
|
|
|
+ ['生成 payload', '整理成统一入库格式'],
|
|
|
+]
|
|
|
+
|
|
|
function mediaUrl(asset) {
|
|
|
return asset?.cdn_url || asset?.oss_url || asset?.source_url || ''
|
|
|
}
|
|
|
|
|
|
+function payloadTitle(payload) {
|
|
|
+ return payload?.title || payload?.source?.title || '未命名 payload'
|
|
|
+}
|
|
|
+
|
|
|
function compactJson(value) {
|
|
|
return JSON.stringify(value || {}, null, 2)
|
|
|
}
|
|
|
|
|
|
-function particleTone(type) {
|
|
|
- return {
|
|
|
- what: 'what',
|
|
|
- how: 'how',
|
|
|
- why: 'why',
|
|
|
- }[type] || 'what'
|
|
|
+function stageList(value) {
|
|
|
+ if (Array.isArray(value)) return value
|
|
|
+ return value ? [value] : []
|
|
|
}
|
|
|
|
|
|
-function typeLabel(type) {
|
|
|
- return {
|
|
|
- what: 'What I know',
|
|
|
- how: 'How I do',
|
|
|
- why: 'Why it works',
|
|
|
- }[type] || type || 'Knowledge'
|
|
|
+function sourceImages(item) {
|
|
|
+ return (item?.media_assets || [])
|
|
|
+ .filter((asset) => ['image', 'cover', 'frame'].includes(asset.media_type))
|
|
|
+ .map(mediaUrl)
|
|
|
+ .filter(Boolean)
|
|
|
}
|
|
|
|
|
|
-function payloadTitle(payload) {
|
|
|
- return payload?.title || payload?.source?.title || '未命名 payload'
|
|
|
+function sourceVideo(item) {
|
|
|
+ const asset = (item?.media_assets || []).find((row) => row.media_type === 'video')
|
|
|
+ return asset ? mediaUrl(asset) : ''
|
|
|
+}
|
|
|
+
|
|
|
+function scopeClass(scopeType) {
|
|
|
+ return `s-${scopeType || 'intent'}`
|
|
|
}
|
|
|
|
|
|
-function PayloadCard({ draft, ingest }) {
|
|
|
- const payload = draft.payload || {}
|
|
|
+function ScopeChip({ scope, showType = false }) {
|
|
|
return (
|
|
|
- <article className="payload-card">
|
|
|
- <div className="payload-head">
|
|
|
- <b>{payloadTitle(payload)}</b>
|
|
|
- <span className={`pill ${draft.status}`}>{draft.status}</span>
|
|
|
- </div>
|
|
|
- <div className="payload-meta">
|
|
|
- <span>{(payload.dim_attributes || []).join(' / ') || '未标属性'}</span>
|
|
|
- <span>{draft.ingest_ready ? '可入库' : '待审核'}</span>
|
|
|
- {ingest && <span>{ingest.target_system} · {ingest.status}</span>}
|
|
|
- </div>
|
|
|
- {payload.content && <pre className="payload-content">{payload.content}</pre>}
|
|
|
- {(payload.scopes || []).length > 0 && (
|
|
|
- <div className="scope-row">
|
|
|
- {payload.scopes.map((scope, idx) => (
|
|
|
- <span className="scope-chip" key={`${scope.scope_type}-${scope.value}-${idx}`}>
|
|
|
- {scope.scope_type}: {scope.value}
|
|
|
- </span>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </article>
|
|
|
+ <span className={`sv ${scopeClass(scope.scope_type)}`}>
|
|
|
+ {showType && <span className="k">{SC_CN[scope.scope_type] || scope.scope_type}</span>}
|
|
|
+ {scope.scope_value || scope.value}
|
|
|
+ <span className="mk mk-new">新</span>
|
|
|
+ </span>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function KnowledgeCard({ particle, scopes }) {
|
|
|
- const content = particle.content || {}
|
|
|
- const steps = content.steps || []
|
|
|
- const body = content.body || []
|
|
|
+function KScopes({ scopes }) {
|
|
|
+ if (!scopes?.length) return null
|
|
|
return (
|
|
|
- <article className={`knowledge-card ${particleTone(particle.particle_type)}`}>
|
|
|
- <div className="knowledge-head">
|
|
|
- <span>{typeLabel(particle.particle_type)}</span>
|
|
|
- <b>{particle.title || '未命名知识颗'}</b>
|
|
|
+ <div className="kscopes">
|
|
|
+ <span className="lab">作用域(5 棵树):</span>
|
|
|
+ {scopes.map((scope, idx) => <ScopeChip key={`${scope.scope_type}-${scope.scope_value}-${idx}`} scope={scope} showType />)}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function Ribbon() {
|
|
|
+ return (
|
|
|
+ <div className="ribbon">
|
|
|
+ <div className="rcap">这套系统怎么「从帖子拆解知识」——从真实单例跑出的状态回看</div>
|
|
|
+ <div className="rsteps">
|
|
|
+ {STEPS.map(([label, desc], idx) => (
|
|
|
+ <React.Fragment key={label}>
|
|
|
+ {idx > 0 && <span className="rarrow">→</span>}
|
|
|
+ <div className={`rstep ${idx === 5 ? 'auto' : ''}`}>
|
|
|
+ <div className="rtop"><span className="rn">{idx + 1}</span><span className="rlab">{label}</span></div>
|
|
|
+ <div className="rdesc">{desc}</div>
|
|
|
+ <div className="rbtn">{idx === 5 ? '⚙️ 程序自动' : '已完成'}</div>
|
|
|
+ </div>
|
|
|
+ </React.Fragment>
|
|
|
+ ))}
|
|
|
</div>
|
|
|
- <div className="knowledge-meta">
|
|
|
- {particle.business_stage && <span>业务阶段:{particle.business_stage}</span>}
|
|
|
- {particle.creation_stage && <span>创作阶段:{particle.creation_stage}</span>}
|
|
|
- <span>{particle.status}</span>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function SourceDetails({ item, readText, onZoom }) {
|
|
|
+ const images = sourceImages(item)
|
|
|
+ const video = sourceVideo(item)
|
|
|
+ return (
|
|
|
+ <details className="src" open>
|
|
|
+ <summary>原帖:{item?.title || '无标题'}(这一帖的原料)· {item?.platform || 'unknown'}{video ? ' · 视频' : images.length ? ` · ${images.length} 图` : ''}</summary>
|
|
|
+ <div className="srcbody">
|
|
|
+ {item?.canonical_url && <a href={item.canonical_url} target="_blank" rel="noreferrer">{item.canonical_url}</a>}
|
|
|
+ {video ? (
|
|
|
+ <div className="srcvideo"><video src={video} controls /></div>
|
|
|
+ ) : (
|
|
|
+ <div className="srcimgs">
|
|
|
+ {images.map((url, idx) => <img key={url} src={url} title={url} onClick={() => onZoom(url)} alt={`原帖图 ${idx + 1}`} />)}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {readText && <div className="srcread">① 读懂后的内容:{readText}</div>}
|
|
|
</div>
|
|
|
- {content.purpose && <p className="knowledge-purpose">{content.purpose}</p>}
|
|
|
- {steps.length > 0 && (
|
|
|
- <div className="step-list">
|
|
|
+ </details>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function HowTable({ knowledge }) {
|
|
|
+ const steps = knowledge.steps || []
|
|
|
+ const scOf = (step, type) => (step['作用域'] || []).filter((scope) => scope.scope_type === type)
|
|
|
+ return (
|
|
|
+ <div className="twrap">
|
|
|
+ <table className="proc">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th className="c-idx" rowSpan="2">#</th>
|
|
|
+ <th className="g-frame" colSpan="5">② 拆框架</th>
|
|
|
+ <th className="g-scope" colSpan="5">③ 作用域</th>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <th className="c-intent">输入</th>
|
|
|
+ <th className="c-cstage cstage-h">创作<br />阶段 ▸</th>
|
|
|
+ <th className="c-act">动作<br />(模型推导)</th>
|
|
|
+ <th className="c-dir">方法</th>
|
|
|
+ <th className="c-out">产出</th>
|
|
|
+ {SCOPES.map(([key, label]) => <th key={key} className={`c-scope s-${key}`}>{label}</th>)}
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
{steps.map((step, idx) => (
|
|
|
- <div className="step-row" key={step.id || idx}>
|
|
|
- <span className="step-no">{idx + 1}</span>
|
|
|
- <div>
|
|
|
- <div><b>输入</b>{step.input || '未写'}</div>
|
|
|
- <div><b>指引</b>{step.directive || '未写'}</div>
|
|
|
- <div><b>产出</b>{step.output || '未写'}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <tr className="step" key={step.id || idx}>
|
|
|
+ <td className="idx">{idx + 1}</td>
|
|
|
+ <td className="intent"><span className="in-txt">{step.input || '未写'}</span></td>
|
|
|
+ <td className="cstage">{step['创作阶段'] && <span className="cstage-chip">{step['创作阶段']}</span>}</td>
|
|
|
+ <td className="act">{step['动作'] && <span className="act-chip">{step['动作']}</span>}</td>
|
|
|
+ <td className="dir"><span className="dir-txt">{step.directive || '未写'}</span></td>
|
|
|
+ <td className="out"><span className="out-txt">{step.output || '未写'}</span></td>
|
|
|
+ {SCOPES.map(([key]) => (
|
|
|
+ <td key={key} className={`scope s-${key}`}>
|
|
|
+ {scOf(step, key).map((scope, scopeIdx) => <ScopeChip key={scopeIdx} scope={scope} />)}
|
|
|
+ </td>
|
|
|
+ ))}
|
|
|
+ </tr>
|
|
|
))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function WhatCard({ knowledge, scopes }) {
|
|
|
+ const body = knowledge.body || []
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ {(knowledge['概要'] || knowledge.kind) && (
|
|
|
+ <div className="def">
|
|
|
+ {knowledge.kind && <span className={`kindb kind-${knowledge.kind}`}>{knowledge.kind}</span>}
|
|
|
+ {knowledge['维度拆分规则'] && <span className="dimrule">按「{knowledge['维度拆分规则']}」拆</span>}
|
|
|
+ {knowledge['概要'] && <>概要:{knowledge['概要']}</>}
|
|
|
</div>
|
|
|
)}
|
|
|
{body.length > 0 && (
|
|
|
- <div className="what-body">
|
|
|
- {body.map((row, idx) => (
|
|
|
- <div className="what-row" key={`${row.item_name || 'row'}-${idx}`}>
|
|
|
- <b>{row.item_name || `条目 ${idx + 1}`}</b>
|
|
|
- <span>{row.item_desc || '暂无描述'}</span>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- {(content['阐述'] || content.desc) && (
|
|
|
- <p className="knowledge-purpose">{content['阐述'] || content.desc}</p>
|
|
|
- )}
|
|
|
- {scopes.length > 0 && (
|
|
|
- <div className="scope-row">
|
|
|
- {scopes.map((scope) => (
|
|
|
- <span className="scope-chip" key={scope.id}>
|
|
|
- {scope.scope_type}: {scope.scope_value}
|
|
|
- </span>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
+ <table className="elem">
|
|
|
+ <tbody>
|
|
|
+ {body.map((row, idx) => (
|
|
|
+ <tr key={`${row.item_name || 'row'}-${idx}`}>
|
|
|
+ <td className="el">{row.item_name || `条目 ${idx + 1}`}</td>
|
|
|
+ <td>{row.item_desc || '暂无描述'}</td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
)}
|
|
|
- <details className="fold knfold">
|
|
|
- <summary>原始知识颗 JSON</summary>
|
|
|
- <pre className="jsonbox">{compactJson(content)}</pre>
|
|
|
- </details>
|
|
|
- </article>
|
|
|
+ <KScopes scopes={scopes} />
|
|
|
+ </div>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function SourcePanel({ item }) {
|
|
|
- const media = item?.media_assets || []
|
|
|
- const images = media.filter((asset) => ['image', 'cover', 'frame'].includes(asset.media_type)).map(mediaUrl).filter(Boolean)
|
|
|
- const video = media.find((asset) => asset.media_type === 'video')
|
|
|
+function WhyCard({ knowledge, scopes }) {
|
|
|
return (
|
|
|
- <section className="source-panel">
|
|
|
- <div>
|
|
|
- <h1>{item?.title || 'Decode 知识详情'}</h1>
|
|
|
- <div className="sub">
|
|
|
- {item?.platform || 'unknown'} · {item?.author_name || '未知作者'} · {item?.content_type || '未知类型'}
|
|
|
- </div>
|
|
|
- {item?.raw_summary && <p className="source-summary">{item.raw_summary}</p>}
|
|
|
- {item?.canonical_url && <a className="btn ghost" href={item.canonical_url} target="_blank" rel="noreferrer">打开原帖</a>}
|
|
|
+ <div>
|
|
|
+ {(knowledge['阐述'] || knowledge.desc) && <div className="why-exp">{knowledge['阐述'] || knowledge.desc}</div>}
|
|
|
+ <KScopes scopes={scopes} />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function KnowledgeCard({ particle, idx, scopes, payload, onJson }) {
|
|
|
+ const knowledge = particle.content || {}
|
|
|
+ const type = particle.particle_type || knowledge.type || 'what'
|
|
|
+ return (
|
|
|
+ <div className="kcard" id={`k-${particle.id}`}>
|
|
|
+ <div className="khead">
|
|
|
+ <span className="kno">第{idx + 1}颗知识</span>
|
|
|
+ <span className={`ktype ty-${type}`}>{TYPE_CN[type] || type}</span>
|
|
|
+ <span className="ktitle">{particle.title || knowledge.title || '未命名知识'}</span>
|
|
|
+ <span className="krole">{TYPE_LABEL[type] || type}</span>
|
|
|
+ {stageList(particle.business_stage || knowledge['业务阶段']).map((stage) => <span className="kbiz" key={stage}>业务阶段:{stage}</span>)}
|
|
|
+ <span className="kacts">{payload && <button className="lbtn" onClick={() => onJson(payload.payload)}>⚙️ 入库 payload</button>}</span>
|
|
|
</div>
|
|
|
- <div className="source-media">
|
|
|
- {video ? (
|
|
|
- <video controls playsInline src={mediaUrl(video)} />
|
|
|
- ) : images.length > 0 ? (
|
|
|
- <div className="decode-thumbs">
|
|
|
- {images.slice(0, 6).map((url, idx) => <img key={url} src={url} alt={`素材 ${idx + 1}`} loading="lazy" />)}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="media-empty">暂无媒体</div>
|
|
|
- )}
|
|
|
+ <div className="kbody">
|
|
|
+ {type === 'how' && <HowTable knowledge={knowledge} />}
|
|
|
+ {type === 'what' && <WhatCard knowledge={knowledge} scopes={scopes} />}
|
|
|
+ {type === 'why' && <WhyCard knowledge={knowledge} scopes={scopes} />}
|
|
|
</div>
|
|
|
- </section>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function PayloadModal({ payload, onClose }) {
|
|
|
+ const [copied, setCopied] = useState(false)
|
|
|
+ if (!payload) return null
|
|
|
+ const copy = () => {
|
|
|
+ navigator.clipboard?.writeText(compactJson(payload)).then(() => {
|
|
|
+ setCopied(true)
|
|
|
+ setTimeout(() => setCopied(false), 1200)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <div className="modal" onClick={onClose}>
|
|
|
+ <div className="mbox" onClick={(event) => event.stopPropagation()}>
|
|
|
+ <h2>
|
|
|
+ ⚙️ ingest payload(给机器看)
|
|
|
+ <button className="lbtn copybtn" onClick={copy}>{copied ? '✓ 已复制' : '📋 复制 JSON'}</button>
|
|
|
+ <span className="x" onClick={onClose}>×</span>
|
|
|
+ </h2>
|
|
|
+ <div className="body"><pre>{compactJson(payload)}</pre></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
export default function DecodeItemDetail({ itemId }) {
|
|
|
const [data, setData] = useState(null)
|
|
|
const [err, setErr] = useState('')
|
|
|
+ const [jsonPayload, setJsonPayload] = useState(null)
|
|
|
+ const [zoom, setZoom] = useState(null)
|
|
|
|
|
|
useEffect(() => {
|
|
|
setData(null)
|
|
|
@@ -170,88 +267,52 @@ export default function DecodeItemDetail({ itemId }) {
|
|
|
return map
|
|
|
}, [data])
|
|
|
|
|
|
- const ingestByDraft = useMemo(() => {
|
|
|
+ const payloadByParticle = useMemo(() => {
|
|
|
const map = new Map()
|
|
|
- for (const record of data?.ingest_records || []) {
|
|
|
- if (record.payload_draft_id) map.set(record.payload_draft_id, record)
|
|
|
+ for (const draft of data?.payload_drafts || []) {
|
|
|
+ if (draft.particle_id) map.set(draft.particle_id, draft)
|
|
|
}
|
|
|
return map
|
|
|
}, [data])
|
|
|
|
|
|
- if (err) return <div className="empty">{err}</div>
|
|
|
- if (!data) return <div className="empty">加载中...</div>
|
|
|
+ if (err) return <div className="legacy-decode-page"><div className="empty">{err}</div></div>
|
|
|
+ if (!data) return <div className="legacy-decode-page"><div className="empty">加载中...</div></div>
|
|
|
|
|
|
- const readText = data.decode_result?.read_result?.text || ''
|
|
|
const particles = data.knowledge_particles || []
|
|
|
- const drafts = data.payload_drafts || []
|
|
|
+ const readText = data.decode_result?.read_result?.text || ''
|
|
|
+ const pass = data.decode_result?.gate_result?.passed
|
|
|
|
|
|
return (
|
|
|
- <div>
|
|
|
- <div className="detail-top">
|
|
|
- <a className="back" href="#/">返回 Query 列表</a>
|
|
|
- <span className="meta">item_id: {itemId}</span>
|
|
|
- </div>
|
|
|
- <SourcePanel item={data.item} />
|
|
|
-
|
|
|
- <section className="decode-strip">
|
|
|
- <div>
|
|
|
- <span>Decode</span>
|
|
|
- <b>{data.decode_result.status}</b>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <span>Knowledge</span>
|
|
|
- <b>{particles.length}</b>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <span>Payload</span>
|
|
|
- <b>{drafts.length}</b>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <span>Ingest</span>
|
|
|
- <b>{data.ingest_records?.filter((row) => row.status === 'ingested').length || 0}</b>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section className="decode-section">
|
|
|
- <h2>单帖读懂</h2>
|
|
|
- <div className="read-panel">{readText || '暂无读懂文本'}</div>
|
|
|
- <div className="gate-panel">
|
|
|
- <span className={`pill ${data.decode_result.gate_result?.passed ? 'decoded' : 'rejected'}`}>
|
|
|
- {data.decode_result.gate_result?.passed ? '通过创作闸' : '未通过创作闸'}
|
|
|
- </span>
|
|
|
- <span>{data.decode_result.gate_result?.reason || '暂无判断理由'}</span>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section className="decode-section">
|
|
|
- <h2>知识颗</h2>
|
|
|
- {particles.length === 0 ? (
|
|
|
- <div className="row-empty">暂无 What/How/Why 知识颗</div>
|
|
|
- ) : (
|
|
|
- <div className="knowledge-grid">
|
|
|
- {particles.map((particle) => (
|
|
|
- <KnowledgeCard
|
|
|
- key={particle.id}
|
|
|
- particle={particle}
|
|
|
- scopes={scopesByParticle.get(particle.id) || []}
|
|
|
- />
|
|
|
- ))}
|
|
|
+ <div className="legacy-decode-page">
|
|
|
+ <div className="app">
|
|
|
+ <header className="top">
|
|
|
+ <h1>创作知识的解构</h1>
|
|
|
+ <div className="picker">
|
|
|
+ <a className="pill" href="#/">返回 Query Demo</a>
|
|
|
+ <span className="pill on">{data.item?.title || '真实单例'}<span className="n">{particles.length}颗</span></span>
|
|
|
+ <span className="lab">状态:{data.decode_result?.status || 'unknown'} · {pass ? '通过创作闸' : '未通过创作闸'}</span>
|
|
|
</div>
|
|
|
- )}
|
|
|
- </section>
|
|
|
-
|
|
|
- <section className="decode-section">
|
|
|
- <h2>Payload Draft</h2>
|
|
|
- {drafts.length === 0 ? (
|
|
|
- <div className="row-empty">暂无 payload draft</div>
|
|
|
- ) : (
|
|
|
- <div className="payload-grid">
|
|
|
- {drafts.map((draft) => (
|
|
|
- <PayloadCard key={draft.id} draft={draft} ingest={ingestByDraft.get(draft.id)} />
|
|
|
- ))}
|
|
|
+ </header>
|
|
|
+ <div className="scroll">
|
|
|
+ <Ribbon />
|
|
|
+ <SourceDetails item={data.item} readText={readText} onZoom={setZoom} />
|
|
|
+ <div className="barhint">
|
|
|
+ 这一帖拆出 <b>{particles.length}</b> 颗知识(how 工序表 / what 知识卡 / why 论点卡);每颗知识右上角可打开 payload。
|
|
|
</div>
|
|
|
- )}
|
|
|
- </section>
|
|
|
+ {particles.map((particle, idx) => (
|
|
|
+ <KnowledgeCard
|
|
|
+ key={particle.id}
|
|
|
+ particle={particle}
|
|
|
+ idx={idx}
|
|
|
+ scopes={scopesByParticle.get(particle.id) || []}
|
|
|
+ payload={payloadByParticle.get(particle.id)}
|
|
|
+ onJson={setJsonPayload}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <PayloadModal payload={jsonPayload} onClose={() => setJsonPayload(null)} />
|
|
|
+ {zoom && <div className="lb" onClick={() => setZoom(null)}><img src={zoom} alt="" /></div>}
|
|
|
</div>
|
|
|
)
|
|
|
}
|