Render submit_card card parts as CardAnchor with inline full-text expansion (CF1) #20
@@ -5,8 +5,8 @@ import { marked } from 'marked'
|
||||
import { api } from '../api'
|
||||
import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store'
|
||||
import { liveToMessage, showProcessInline } from '../live'
|
||||
import type { Message, Part, HITLCard } from '../contracts'
|
||||
import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui'
|
||||
import type { Message, Part, HITLCard, ActivityCard } from '../contracts'
|
||||
import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
|
||||
import { docBridge } from './Workbench'
|
||||
import { groupParts, artifactIcon } from '../messageparts'
|
||||
|
||||
@@ -55,8 +55,16 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
|
||||
case 'plan':
|
||||
return null // 计划在右栏 Plan 标签呈现
|
||||
case 'card': {
|
||||
// cardlink 互定位(D32):点击 ↗ 定位收件箱对应卡(从 data 中解析 card:<id>)。
|
||||
const m = /card:([A-Za-z0-9_]+)/.exec(JSON.stringify(part.card?.data ?? ''))
|
||||
// 卡流锚点(D39):cardId 非空 = submit_card 引用部件——中栏自足(可展开全文)+ 跳收件箱。
|
||||
if (part.card?.cardId) {
|
||||
const ref = part.card.cardId
|
||||
const card = findCard(ref)
|
||||
const bodyChars = card?.body?.reduce((n, node) => n + (node.markdown?.length ?? 0), 0)
|
||||
return <CardAnchor title={card?.title ?? part.card.kind} chars={bodyChars} card={card}
|
||||
onLocate={() => store.openInbox(ref)} docBridge={docBridge} />
|
||||
}
|
||||
// 插件内嵌卡片部件(cardlink 互定位,D32):点击 ↗ 定位收件箱对应卡(从 data 解析 card:<id>)。
|
||||
const m = JSON.stringify(part.card?.data ?? '').match(/card:([A-Za-z0-9_]+)/)
|
||||
return (
|
||||
<div className="ui-section card-link" title={m ? '点击在收件箱中定位' : undefined}
|
||||
onClick={() => m && store.openInbox(m[1])}>
|
||||
@@ -77,6 +85,12 @@ function findArtifact(id: string) {
|
||||
return artifactCache[id]
|
||||
}
|
||||
|
||||
// 收件箱卡片查询缓存(store.cards 的派生索引;卡流锚点取卡数据用)。
|
||||
let cardCache: Record<string, ActivityCard> = {}
|
||||
function findCard(id: string) {
|
||||
return cardCache[id]
|
||||
}
|
||||
|
||||
// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。
|
||||
function ProcessCollapse({ parts }: { parts: Part[] }) {
|
||||
const toolCount = parts.filter(p => p.type === 'tool_call').length
|
||||
@@ -131,7 +145,7 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
|
||||
return -1
|
||||
})()
|
||||
return (
|
||||
<div className="msg assistant">
|
||||
<div className="msg assistant" data-msg-id={msg.id}>
|
||||
<div className="msg-role">
|
||||
<span className="role-mark role-assistant"><BrandMark size={16} /></span>
|
||||
<span>助手</span>
|
||||
@@ -261,6 +275,7 @@ export function Conversation() {
|
||||
const bodyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a]))
|
||||
cardCache = Object.fromEntries(cards.map(c => [c.id, c]))
|
||||
|
||||
const active = activeRun(runs)
|
||||
|
||||
@@ -326,7 +341,7 @@ export function Conversation() {
|
||||
// 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。
|
||||
const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm')
|
||||
return (
|
||||
<div className="msg user" key={m.id}>
|
||||
<div className="msg user" key={m.id} data-msg-id={m.id}>
|
||||
<div className="msg-role">
|
||||
<span className="role-mark"><Avatar name={myName} size="sm" /></span>你
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function Workbench() {
|
||||
onAction: (c: ActivityCard, id: string) => store.cardAction(c, id),
|
||||
onPin: (c: ActivityCard, p: boolean) => store.cardPin(c, p),
|
||||
onDismiss: (c: ActivityCard) => store.cardDismiss(c),
|
||||
onLocate: (c: ActivityCard) => store.openInbox(c.id),
|
||||
onLocate: (c: ActivityCard) => store.locateMessage(c.anchorMessageId),
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export interface Part {
|
||||
artifact?: { artifactId: ID; name: string; kind?: string };
|
||||
hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string };
|
||||
plan?: PlanSnapshot;
|
||||
card?: { kind: string; data: UINode[] };
|
||||
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
|
||||
error?: { code: string; message: string };
|
||||
process?: boolean; // run 终态后折叠为摘要行
|
||||
}
|
||||
|
||||
@@ -503,6 +503,13 @@ export const store = {
|
||||
document.querySelector(`[data-card-id="${cardId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 60)
|
||||
},
|
||||
// 卡→消息互定位另一半(D32):从收件箱卡滚回产生它的对话消息。
|
||||
locateMessage(messageId?: string) {
|
||||
if (!messageId) return
|
||||
setTimeout(() => {
|
||||
document.querySelector(`[data-msg-id="${messageId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 60)
|
||||
},
|
||||
openTracker(runId: string) { setUI({ trackerRunId: runId }) },
|
||||
viewArtifact(a: Artifact | null) { setUI({ viewingArtifact: a }) },
|
||||
openSettings(open: boolean) { setUI({ settingsOpen: open }) },
|
||||
|
||||
Reference in New Issue
Block a user