sparkdesign 0.4.6 → 0.4.7
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/AI_README.md +60 -0
- package/README.md +1 -1
- package/cli/dist/commands/add.js +1 -1
- package/cli/dist/commands/init.js +1 -1
- package/cli/registry/AGENTS.md +9 -2
- package/cli/registry/agent-manifest.json +794 -0
- package/cli/registry/basic/alert.tsx +76 -0
- package/cli/registry/basic/aspect-ratio.tsx +8 -0
- package/cli/registry/basic/breadcrumb.tsx +117 -0
- package/cli/registry/basic/button-group.tsx +79 -0
- package/cli/registry/basic/button.tsx +1 -1
- package/cli/registry/basic/calendar.tsx +221 -0
- package/cli/registry/basic/card.tsx +103 -0
- package/cli/registry/basic/carousel.tsx +241 -0
- package/cli/registry/basic/chart.tsx +372 -0
- package/cli/registry/basic/checkbox.tsx +42 -0
- package/cli/registry/basic/collapsible-card.tsx +2 -2
- package/cli/registry/basic/combobox.tsx +75 -0
- package/cli/registry/basic/command.tsx +184 -0
- package/cli/registry/basic/context-menu.tsx +239 -0
- package/cli/registry/basic/data-table.tsx +73 -0
- package/cli/registry/basic/date-picker.tsx +13 -0
- package/cli/registry/basic/dialog.tsx +169 -0
- package/cli/registry/basic/direction.tsx +25 -0
- package/cli/registry/basic/drawer.tsx +164 -0
- package/cli/registry/basic/dropdown-menu.tsx +0 -4
- package/cli/registry/basic/empty.tsx +104 -0
- package/cli/registry/basic/field.tsx +248 -0
- package/cli/registry/basic/hover-card.tsx +58 -0
- package/cli/registry/basic/input-group.tsx +168 -0
- package/cli/registry/basic/input-otp.tsx +75 -0
- package/cli/registry/basic/input.tsx +27 -0
- package/cli/registry/basic/item.tsx +204 -0
- package/cli/registry/basic/label.tsx +24 -0
- package/cli/registry/basic/menubar.tsx +274 -0
- package/cli/registry/basic/native-select.tsx +62 -0
- package/cli/registry/basic/navigation-menu.tsx +168 -0
- package/cli/registry/basic/popover.tsx +59 -0
- package/cli/registry/basic/scroll-area.tsx +58 -0
- package/cli/registry/basic/select.tsx +2 -1
- package/cli/registry/basic/separator.tsx +26 -0
- package/cli/registry/basic/sheet.tsx +18 -0
- package/cli/registry/basic/textarea.tsx +25 -0
- package/cli/registry/basic/toggle.tsx +1 -1
- package/cli/registry/basic/typography.tsx +1 -1
- package/cli/registry/chat/chat-input/chat-input-textarea.tsx +1 -1
- package/cli/registry/chat/chat-input/compound.tsx +4 -3
- package/cli/registry/chat/chat-input/context.tsx +4 -1
- package/cli/registry/chat/code-block-part.tsx +1 -1
- package/cli/registry/chat/conversation-anchor-nav.tsx +349 -0
- package/cli/registry/chat/file-attachment.tsx +2 -1
- package/cli/registry/chat/file-review-part.tsx +21 -21
- package/cli/registry/chat/markdown.tsx +2 -2
- package/cli/registry/chat/queue-indicator.tsx +1 -0
- package/cli/registry/chat/streaming-markdown-block.tsx +12 -8
- package/cli/registry/chat/tool-invocation-card.tsx +4 -1
- package/cli/registry/lib/file-icon-maps.ts +22 -22
- package/cli/registry/meta.json +518 -0
- package/cli/registry/tokens/ontology.json +404 -0
- package/cli/registry/tokens/scale/presets/compact.css +16 -5
- package/cli/registry/tokens/scale/presets/dense.css +13 -2
- package/cli/registry/tokens/scale/presets/sharp.css +18 -6
- package/cli/registry/tokens/scale/presets/soft.css +23 -1
- package/dist/registry/basic/alert.d.ts +24 -0
- package/dist/registry/basic/aspect-ratio.d.ts +16 -0
- package/dist/registry/basic/breadcrumb.d.ts +24 -0
- package/dist/registry/basic/button-group.d.ts +26 -0
- package/dist/registry/basic/button.d.ts +1 -1
- package/dist/registry/basic/calendar.d.ts +22 -0
- package/dist/registry/basic/card.d.ts +27 -0
- package/dist/registry/basic/carousel.d.ts +19 -0
- package/dist/registry/basic/chart.d.ts +55 -0
- package/dist/registry/basic/checkbox.d.ts +21 -0
- package/dist/registry/basic/combobox.d.ts +26 -0
- package/dist/registry/basic/command.d.ts +18 -0
- package/dist/registry/basic/context-menu.d.ts +44 -0
- package/dist/registry/basic/data-table.d.ts +26 -0
- package/dist/registry/basic/date-picker.d.ts +18 -0
- package/dist/registry/basic/dialog.d.ts +39 -0
- package/dist/registry/basic/direction.d.ts +19 -0
- package/dist/registry/basic/drawer.d.ts +37 -0
- package/dist/registry/basic/empty.d.ts +22 -0
- package/dist/registry/basic/field.d.ts +24 -0
- package/dist/registry/basic/hover-card.d.ts +22 -0
- package/dist/registry/basic/input-group.d.ts +27 -0
- package/dist/registry/basic/input-otp.d.ts +22 -0
- package/dist/registry/basic/input.d.ts +15 -0
- package/dist/registry/basic/item.d.ts +34 -0
- package/dist/registry/basic/label.d.ts +16 -0
- package/dist/registry/basic/menubar.d.ts +37 -0
- package/dist/registry/basic/native-select.d.ts +18 -0
- package/dist/registry/basic/navigation-menu.d.ts +25 -0
- package/dist/registry/basic/popover.d.ts +23 -0
- package/dist/registry/basic/scroll-area.d.ts +5 -0
- package/dist/registry/basic/separator.d.ts +16 -0
- package/dist/registry/basic/sheet.d.ts +13 -0
- package/dist/registry/basic/textarea.d.ts +15 -0
- package/dist/registry/basic/toggle.d.ts +1 -1
- package/dist/registry/chat/chat-input/context.d.ts +3 -1
- package/dist/registry/chat/conversation-anchor-nav.d.ts +72 -0
- package/dist/registry/chat/tool-invocation-card.d.ts +2 -0
- package/dist/scale/presets/compact.css +16 -5
- package/dist/scale/presets/dense.css +13 -2
- package/dist/scale/presets/sharp.css +18 -6
- package/dist/scale/presets/soft.css +23 -1
- package/dist/spark-design.cjs.js +40 -36
- package/dist/spark-design.es.js +8647 -8657
- package/dist/sparkdesign.css +1 -1
- package/dist/src/components/basic/Alert/index.d.ts +13 -0
- package/dist/src/components/basic/AspectRatio/index.d.ts +13 -0
- package/dist/src/components/basic/Breadcrumb/index.d.ts +12 -0
- package/dist/src/components/basic/ButtonGroup/index.d.ts +13 -0
- package/dist/src/components/basic/Calendar/index.d.ts +13 -0
- package/dist/src/components/basic/Card/index.d.ts +13 -0
- package/dist/src/components/basic/Carousel/index.d.ts +12 -0
- package/dist/src/components/basic/Chart/index.d.ts +13 -0
- package/dist/src/components/basic/Checkbox/index.d.ts +13 -0
- package/dist/src/components/basic/Combobox/index.d.ts +13 -0
- package/dist/src/components/basic/Command/index.d.ts +12 -0
- package/dist/src/components/basic/ContextMenu/index.d.ts +19 -0
- package/dist/src/components/basic/DataTable/index.d.ts +13 -0
- package/dist/src/components/basic/DatePicker/index.d.ts +13 -0
- package/dist/src/components/basic/Dialog/index.d.ts +16 -0
- package/dist/src/components/basic/Direction/index.d.ts +13 -0
- package/dist/src/components/basic/Drawer/index.d.ts +16 -0
- package/dist/src/components/basic/Empty/index.d.ts +12 -0
- package/dist/src/components/basic/Field/index.d.ts +12 -0
- package/dist/src/components/basic/HoverCard/index.d.ts +16 -0
- package/dist/src/components/basic/Input/index.d.ts +13 -0
- package/dist/src/components/basic/InputGroup/index.d.ts +12 -0
- package/dist/src/components/basic/InputOTP/index.d.ts +12 -0
- package/dist/src/components/basic/Item/index.d.ts +12 -0
- package/dist/src/components/basic/Label/index.d.ts +13 -0
- package/dist/src/components/basic/Menubar/index.d.ts +12 -0
- package/dist/src/components/basic/NativeSelect/index.d.ts +12 -0
- package/dist/src/components/basic/NavigationMenu/index.d.ts +12 -0
- package/dist/src/components/basic/Popover/index.d.ts +16 -0
- package/dist/src/components/basic/ScrollArea/index.d.ts +12 -0
- package/dist/src/components/basic/Separator/index.d.ts +13 -0
- package/dist/src/components/basic/Sheet/index.d.ts +13 -0
- package/dist/src/components/basic/Textarea/index.d.ts +13 -0
- package/dist/src/components/chat/ConversationAnchorNav/index.d.ts +13 -0
- package/dist/src/components/chat/StreamingMarkdownBlock/index.d.ts +13 -0
- package/dist/src/components/index.d.ts +57 -0
- package/dist/tokens/AGENTS.md +1 -0
- package/dist/tokens/scale/presets/compact.css +16 -5
- package/dist/tokens/scale/presets/dense.css +13 -2
- package/dist/tokens/scale/presets/sharp.css +18 -6
- package/dist/tokens/scale/presets/soft.css +23 -1
- package/docs/agent/component-selection.md +60 -0
- package/docs/agent/token-ontology.md +37 -0
- package/package.json +31 -5
- package/registry/agent-manifest.json +794 -0
- package/registry/tokens/ontology.json +404 -0
- package/dist/_basePickBy-DnQN8w3y.js +0 -151
- package/dist/_basePickBy-a-kPMlkg.cjs +0 -1
- package/dist/_baseUniq-B-N2NQ50.js +0 -614
- package/dist/_baseUniq-Cc_zbSif.cjs +0 -1
- package/dist/arc-BQBhijZ6.js +0 -83
- package/dist/arc-mWQt0Yph.cjs +0 -1
- package/dist/architectureDiagram-VXUJARFQ-BMZEucno.cjs +0 -36
- package/dist/architectureDiagram-VXUJARFQ-DTdjD3Bp.js +0 -4661
- package/dist/blockDiagram-VD42YOAC-CzHn0yob.js +0 -2256
- package/dist/blockDiagram-VD42YOAC-DDxdHAlz.cjs +0 -122
- package/dist/c4Diagram-YG6GDRKO-4Gz0I4gj.cjs +0 -10
- package/dist/c4Diagram-YG6GDRKO-BIy--yVN.js +0 -1580
- package/dist/channel-BQn0o8bs.js +0 -5
- package/dist/channel-DaN7XniJ.cjs +0 -1
- package/dist/chunk-4BX2VUAB-BlQFTQqz.cjs +0 -1
- package/dist/chunk-4BX2VUAB-Czitj3Kc.js +0 -8
- package/dist/chunk-55IACEB6-DXacNZbO.js +0 -8
- package/dist/chunk-55IACEB6-DnDxpye9.cjs +0 -1
- package/dist/chunk-B4BG7PRW-CBdN0q_V.js +0 -1375
- package/dist/chunk-B4BG7PRW-DbGvUkGO.cjs +0 -165
- package/dist/chunk-DI55MBZ5-D1YJMs6x.cjs +0 -220
- package/dist/chunk-DI55MBZ5-NCQTvayw.js +0 -1370
- package/dist/chunk-FMBD7UC4-CsGMbrtr.js +0 -19
- package/dist/chunk-FMBD7UC4-Di7cUUh5.cjs +0 -15
- package/dist/chunk-QN33PNHL-0j5LC8Lm.cjs +0 -1
- package/dist/chunk-QN33PNHL-3GERZBRm.js +0 -19
- package/dist/chunk-QZHKN3VN-AVEY9ImQ.js +0 -15
- package/dist/chunk-QZHKN3VN-s8Z0a8mc.cjs +0 -1
- package/dist/chunk-TZMSLE5B-CAf87HPt.cjs +0 -1
- package/dist/chunk-TZMSLE5B-sbiflal0.js +0 -64
- package/dist/classDiagram-2ON5EDUG-Ct9JLIN2.cjs +0 -1
- package/dist/classDiagram-2ON5EDUG-Dzfrft3a.js +0 -16
- package/dist/classDiagram-v2-WZHVMYZB-Ct9JLIN2.cjs +0 -1
- package/dist/classDiagram-v2-WZHVMYZB-Dzfrft3a.js +0 -16
- package/dist/clone-Cde_NQ8V.js +0 -8
- package/dist/clone-DCNjWuM2.cjs +0 -1
- package/dist/cose-bilkent-S5V4N54A-0uLijMro.cjs +0 -1
- package/dist/cose-bilkent-S5V4N54A-Bb08N431.js +0 -2608
- package/dist/cytoscape.esm-CNUX3VTg.cjs +0 -321
- package/dist/cytoscape.esm-Cvf3sx9F.js +0 -18704
- package/dist/dagre-6UL2VRFP-CY_Wz5Zd.js +0 -444
- package/dist/dagre-6UL2VRFP-Dxe7_qZc.cjs +0 -4
- package/dist/defaultLocale-BgPVtth8.js +0 -171
- package/dist/defaultLocale-C4wbwF1n.cjs +0 -1
- package/dist/diagram-PSM6KHXK-D2bdb7MT.js +0 -531
- package/dist/diagram-PSM6KHXK-YF69SUjY.cjs +0 -24
- package/dist/diagram-QEK2KX5R-BpUSoh0-.js +0 -217
- package/dist/diagram-QEK2KX5R-DZPGteon.cjs +0 -43
- package/dist/diagram-S2PKOQOG-ht-zdvFG.cjs +0 -24
- package/dist/diagram-S2PKOQOG-zFeLJ50Z.js +0 -142
- package/dist/erDiagram-Q2GNP2WA-B38iJ6ts.js +0 -841
- package/dist/erDiagram-Q2GNP2WA-RgS80DDU.cjs +0 -60
- package/dist/flowDiagram-NV44I4VS-BHilOs2p.cjs +0 -162
- package/dist/flowDiagram-NV44I4VS-BrBJcoce.js +0 -1620
- package/dist/ganttDiagram-JELNMOA3-pZiJeFio.cjs +0 -267
- package/dist/ganttDiagram-JELNMOA3-tw6FhkWJ.js +0 -2670
- package/dist/gitGraphDiagram-V2S2FVAM-BWn5uIK5.js +0 -699
- package/dist/gitGraphDiagram-V2S2FVAM-DKKeG-9R.cjs +0 -65
- package/dist/graph-DIbblrZP.cjs +0 -1
- package/dist/graph-DPcK91G3.js +0 -247
- package/dist/infoDiagram-HS3SLOUP-B8gwwhct.cjs +0 -2
- package/dist/infoDiagram-HS3SLOUP-D47PNcP_.js +0 -24
- package/dist/init-CHZsXQcr.cjs +0 -1
- package/dist/init-DjUOC4st.js +0 -16
- package/dist/journeyDiagram-XKPGCS4Q-BG3cfhyU.js +0 -834
- package/dist/journeyDiagram-XKPGCS4Q-D8DVLJof.cjs +0 -139
- package/dist/kanban-definition-3W4ZIXB7-4OCnEouP.cjs +0 -89
- package/dist/kanban-definition-3W4ZIXB7-CWi_ssF9.js +0 -719
- package/dist/layout-Byuh8f-J.cjs +0 -1
- package/dist/layout-CdLdvj1j.js +0 -1335
- package/dist/linear-C2Q_PI9B.js +0 -259
- package/dist/linear-C69aPBW1.cjs +0 -1
- package/dist/mermaid.core-DBwAx_jp.cjs +0 -249
- package/dist/mermaid.core-gFR0XUlD.js +0 -15300
- package/dist/mindmap-definition-VGOIOE7T-8P7obVV4.cjs +0 -68
- package/dist/mindmap-definition-VGOIOE7T-DnOa7WJ9.js +0 -784
- package/dist/ordinal-B6-f3MAq.js +0 -61
- package/dist/ordinal-CagbB1m8.cjs +0 -1
- package/dist/pieDiagram-ADFJNKIX-5NAlvhMo.js +0 -161
- package/dist/pieDiagram-ADFJNKIX-CQBG4yR9.cjs +0 -30
- package/dist/quadrantDiagram-AYHSOK5B-Oe4y7RZ0.cjs +0 -7
- package/dist/quadrantDiagram-AYHSOK5B-rh2DPEP1.js +0 -1022
- package/dist/requirementDiagram-UZGBJVZJ-DcWaCuXr.js +0 -850
- package/dist/requirementDiagram-UZGBJVZJ-gfdlrFiq.cjs +0 -64
- package/dist/sankeyDiagram-TZEHDZUN-CQIKFwD0.js +0 -810
- package/dist/sankeyDiagram-TZEHDZUN-DvPtzQvC.cjs +0 -10
- package/dist/sequenceDiagram-WL72ISMW-BNrsMagL.cjs +0 -145
- package/dist/sequenceDiagram-WL72ISMW-iCX3ckKx.js +0 -2511
- package/dist/stateDiagram-FKZM4ZOC-DBvJ_eeL.cjs +0 -1
- package/dist/stateDiagram-FKZM4ZOC-ZVsJlaHJ.js +0 -263
- package/dist/stateDiagram-v2-4FDKWEC3-CB_nTHcE.js +0 -16
- package/dist/stateDiagram-v2-4FDKWEC3-Xkx17v6T.cjs +0 -1
- package/dist/timeline-definition-IT6M3QCI-BmGkYQiz.cjs +0 -61
- package/dist/timeline-definition-IT6M3QCI-Ck8zTt6w.js +0 -795
- package/dist/treemap-GDKQZRPO-B9sfERx8.js +0 -17922
- package/dist/treemap-GDKQZRPO-BVfJRs0Z.cjs +0 -160
- package/dist/xychartDiagram-PRI3JC2R-By_S8NzN.js +0 -1340
- package/dist/xychartDiagram-PRI3JC2R-CNfDrGxM.cjs +0 -7
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import type { RefObject, ReactNode } from 'react'
|
|
5
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import { Tooltip } from '../basic/tooltip'
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// 对话锚点导航条(Conversation Anchor Nav)
|
|
11
|
+
// ----------------------------------------------------------------------------
|
|
12
|
+
// 固定在长对话流容器右侧居中,为每条用户消息(或任何可定位节点)渲染一枚短横杠
|
|
13
|
+
// 或「打孔」圆点。hover 时横杠加长 / 圆点放大并展示摘要 tooltip,点击触发 onSelect。
|
|
14
|
+
// 当前可视区域对应的锚点自动高亮,数量不足 minItems 或 hidden=true 时自动隐藏。
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/** 单个锚点项 */
|
|
18
|
+
export interface ConversationAnchorItem {
|
|
19
|
+
/** 稳定 id,用于定位 DOM 节点与 React key */
|
|
20
|
+
id: string
|
|
21
|
+
/** 在 Tooltip 中展示的摘要文案(纯文本)。为空时回落到 labels.scrollTo。 */
|
|
22
|
+
summary?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** i18n 文案 */
|
|
26
|
+
export interface ConversationAnchorLabels {
|
|
27
|
+
/** nav 的 aria-label */
|
|
28
|
+
ariaLabel?: string
|
|
29
|
+
/** Tooltip 兜底文案(无摘要时) */
|
|
30
|
+
scrollTo?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 样式变体 */
|
|
34
|
+
export type ConversationAnchorVariant = 'bar' | 'punch'
|
|
35
|
+
|
|
36
|
+
export interface ConversationAnchorNavProps {
|
|
37
|
+
/** 滚动容器 ref,用于检测当前可视锚点;受控场景可传 { current: null } */
|
|
38
|
+
scrollContainerRef: RefObject<HTMLElement | null>
|
|
39
|
+
/** 锚点列表,顺序即视觉顺序 */
|
|
40
|
+
items: ConversationAnchorItem[]
|
|
41
|
+
/** 点击锚点时回调,参数为在 items 中的索引与对应 item */
|
|
42
|
+
onSelect: (index: number, item: ConversationAnchorItem) => void
|
|
43
|
+
/**
|
|
44
|
+
* 当前高亮索引(受控)。
|
|
45
|
+
* 省略时组件内部会订阅 scrollContainerRef 的滚动并通过 IntersectionObserver 自动推导。
|
|
46
|
+
*/
|
|
47
|
+
activeIndex?: number
|
|
48
|
+
/**
|
|
49
|
+
* 锚点视觉样式:
|
|
50
|
+
* - 'bar' (默认):短横杠,hover 时加长
|
|
51
|
+
* - 'punch':圆形"打孔"点,适合羊皮纸 / 拟物风主题
|
|
52
|
+
*/
|
|
53
|
+
variant?: ConversationAnchorVariant
|
|
54
|
+
/** 少于该数量时隐藏整个 nav,默认 3 */
|
|
55
|
+
minItems?: number
|
|
56
|
+
/** 外部强制隐藏(例如 compact 布局),默认 false */
|
|
57
|
+
hidden?: boolean
|
|
58
|
+
/** 自定义 Tooltip / aria 文案 */
|
|
59
|
+
labels?: ConversationAnchorLabels
|
|
60
|
+
/** 额外 className(会与默认定位 className 合并) */
|
|
61
|
+
className?: string
|
|
62
|
+
/**
|
|
63
|
+
* 自定义锚点元素解析。默认通过
|
|
64
|
+
* `scrollContainerRef.current.querySelector('[data-spark-anchor="<id>"]')`
|
|
65
|
+
* 查找;需要业务在消息容器上添加该属性。
|
|
66
|
+
*/
|
|
67
|
+
getAnchorElement?: (
|
|
68
|
+
item: ConversationAnchorItem,
|
|
69
|
+
index: number,
|
|
70
|
+
container: HTMLElement,
|
|
71
|
+
) => HTMLElement | null
|
|
72
|
+
/** 渲染 slot:自定义每个锚点的视觉表达,返回 null 时回落到内置样式 */
|
|
73
|
+
renderAnchor?: (ctx: {
|
|
74
|
+
item: ConversationAnchorItem
|
|
75
|
+
index: number
|
|
76
|
+
isActive: boolean
|
|
77
|
+
variant: ConversationAnchorVariant
|
|
78
|
+
}) => ReactNode
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DEFAULT_LABELS: Required<ConversationAnchorLabels> = {
|
|
82
|
+
ariaLabel: 'Conversation anchors',
|
|
83
|
+
scrollTo: 'Scroll to message',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const ConversationAnchorNav = memo(function ConversationAnchorNav({
|
|
87
|
+
scrollContainerRef,
|
|
88
|
+
items,
|
|
89
|
+
onSelect,
|
|
90
|
+
activeIndex: controlledActiveIndex,
|
|
91
|
+
variant = 'bar',
|
|
92
|
+
minItems = 3,
|
|
93
|
+
hidden = false,
|
|
94
|
+
labels,
|
|
95
|
+
className,
|
|
96
|
+
getAnchorElement,
|
|
97
|
+
renderAnchor,
|
|
98
|
+
}: ConversationAnchorNavProps) {
|
|
99
|
+
const mergedLabels = { ...DEFAULT_LABELS, ...labels }
|
|
100
|
+
|
|
101
|
+
const detectedActiveIndex = useVisibleAnchorIndex({
|
|
102
|
+
scrollContainerRef,
|
|
103
|
+
items,
|
|
104
|
+
getAnchorElement,
|
|
105
|
+
enabled: controlledActiveIndex === undefined && !hidden,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const activeIndex =
|
|
109
|
+
controlledActiveIndex !== undefined ? controlledActiveIndex : detectedActiveIndex
|
|
110
|
+
|
|
111
|
+
const handleClick = useCallback(
|
|
112
|
+
(index: number) => {
|
|
113
|
+
const item = items[index]
|
|
114
|
+
if (item) onSelect(index, item)
|
|
115
|
+
},
|
|
116
|
+
[items, onSelect],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const shouldShow = !hidden && items.length >= minItems
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<AnimatePresence mode="wait">
|
|
123
|
+
{shouldShow && (
|
|
124
|
+
<motion.nav
|
|
125
|
+
key="spark-conversation-anchor-nav"
|
|
126
|
+
initial={{ opacity: 0, x: 8 }}
|
|
127
|
+
animate={{
|
|
128
|
+
opacity: 1,
|
|
129
|
+
x: 0,
|
|
130
|
+
transition: { type: 'spring', stiffness: 500, damping: 30, mass: 0.6 },
|
|
131
|
+
}}
|
|
132
|
+
exit={{
|
|
133
|
+
opacity: 0,
|
|
134
|
+
x: 8,
|
|
135
|
+
transition: { duration: 0.15, ease: [0.32, 0.72, 0, 1] },
|
|
136
|
+
}}
|
|
137
|
+
className={cn(
|
|
138
|
+
'absolute right-3 top-1/2 -translate-y-1/2 z-[40]',
|
|
139
|
+
'flex flex-col items-end gap-1.5 max-h-[80%] overflow-y-auto',
|
|
140
|
+
'scrollbar-hide',
|
|
141
|
+
className,
|
|
142
|
+
)}
|
|
143
|
+
aria-label={mergedLabels.ariaLabel}
|
|
144
|
+
>
|
|
145
|
+
{items.map((item, index) => (
|
|
146
|
+
<AnchorDot
|
|
147
|
+
key={item.id}
|
|
148
|
+
item={item}
|
|
149
|
+
index={index}
|
|
150
|
+
isActive={index === activeIndex}
|
|
151
|
+
variant={variant}
|
|
152
|
+
tooltipFallback={mergedLabels.scrollTo}
|
|
153
|
+
onClick={handleClick}
|
|
154
|
+
renderAnchor={renderAnchor}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</motion.nav>
|
|
158
|
+
)}
|
|
159
|
+
</AnimatePresence>
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
ConversationAnchorNav.displayName = 'ConversationAnchorNav'
|
|
164
|
+
|
|
165
|
+
// ----------------------------------------------------------------------------
|
|
166
|
+
// 单个锚点
|
|
167
|
+
// ----------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
interface AnchorDotProps {
|
|
170
|
+
item: ConversationAnchorItem
|
|
171
|
+
index: number
|
|
172
|
+
isActive: boolean
|
|
173
|
+
variant: ConversationAnchorVariant
|
|
174
|
+
tooltipFallback: string
|
|
175
|
+
onClick: (index: number) => void
|
|
176
|
+
renderAnchor?: ConversationAnchorNavProps['renderAnchor']
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const AnchorDot = memo(
|
|
180
|
+
function AnchorDot({
|
|
181
|
+
item,
|
|
182
|
+
index,
|
|
183
|
+
isActive,
|
|
184
|
+
variant,
|
|
185
|
+
tooltipFallback,
|
|
186
|
+
onClick,
|
|
187
|
+
renderAnchor,
|
|
188
|
+
}: AnchorDotProps) {
|
|
189
|
+
const handleClick = useCallback(() => onClick(index), [onClick, index])
|
|
190
|
+
|
|
191
|
+
const custom = renderAnchor
|
|
192
|
+
? renderAnchor({ item, index, isActive, variant })
|
|
193
|
+
: null
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<Tooltip
|
|
197
|
+
content={item.summary || tooltipFallback}
|
|
198
|
+
side="left"
|
|
199
|
+
contentClassName="max-w-xs"
|
|
200
|
+
>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={handleClick}
|
|
204
|
+
className={cn(
|
|
205
|
+
// 使用命名 group(group/spark-anchor)避免被外层 `.group` 祖先的 hover 污染
|
|
206
|
+
'group/spark-anchor flex cursor-pointer border-0 bg-transparent outline-none',
|
|
207
|
+
'focus-visible:ring-2 focus-visible:ring-[var(--color-link)] focus-visible:rounded-full',
|
|
208
|
+
variant === 'punch'
|
|
209
|
+
? 'h-7 w-7 shrink-0 items-center justify-center p-0'
|
|
210
|
+
: 'items-center justify-end p-1',
|
|
211
|
+
)}
|
|
212
|
+
aria-label={`${tooltipFallback} ${index + 1}`}
|
|
213
|
+
aria-current={isActive ? 'true' : undefined}
|
|
214
|
+
data-spark-anchor-nav-item={item.id}
|
|
215
|
+
>
|
|
216
|
+
{custom ?? (
|
|
217
|
+
<span
|
|
218
|
+
className={cn(
|
|
219
|
+
'block shrink-0 origin-center',
|
|
220
|
+
variant === 'punch'
|
|
221
|
+
? cn(
|
|
222
|
+
'rounded-full transition-transform duration-300 ease-out',
|
|
223
|
+
isActive
|
|
224
|
+
? 'h-2 w-2 bg-[var(--color-link)] group-hover/spark-anchor:scale-125'
|
|
225
|
+
: cn(
|
|
226
|
+
'h-1.5 w-1.5 border border-solid border-border-tertiary bg-transparent',
|
|
227
|
+
'group-hover/spark-anchor:scale-150 group-hover/spark-anchor:border-[var(--color-link)]',
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
: cn(
|
|
231
|
+
'rounded-full transition-all duration-300 ease-out h-0.5',
|
|
232
|
+
isActive
|
|
233
|
+
? 'w-4 bg-text-quaternary'
|
|
234
|
+
: 'w-2.5 bg-border-tertiary',
|
|
235
|
+
'group-hover/spark-anchor:w-6 group-hover/spark-anchor:bg-text-quaternary',
|
|
236
|
+
),
|
|
237
|
+
)}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</button>
|
|
241
|
+
</Tooltip>
|
|
242
|
+
)
|
|
243
|
+
},
|
|
244
|
+
(prev, next) =>
|
|
245
|
+
prev.isActive === next.isActive &&
|
|
246
|
+
prev.variant === next.variant &&
|
|
247
|
+
prev.index === next.index &&
|
|
248
|
+
prev.item.id === next.item.id &&
|
|
249
|
+
prev.item.summary === next.item.summary &&
|
|
250
|
+
prev.tooltipFallback === next.tooltipFallback &&
|
|
251
|
+
prev.onClick === next.onClick &&
|
|
252
|
+
prev.renderAnchor === next.renderAnchor,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
// ----------------------------------------------------------------------------
|
|
256
|
+
// Hook: 基于 IntersectionObserver 推导当前可视锚点索引
|
|
257
|
+
// ----------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
export interface UseVisibleAnchorIndexOptions {
|
|
260
|
+
scrollContainerRef: RefObject<HTMLElement | null>
|
|
261
|
+
items: ConversationAnchorItem[]
|
|
262
|
+
/** 与 ConversationAnchorNav 同名 prop,默认按 `[data-spark-anchor="<id>"]` 查找 */
|
|
263
|
+
getAnchorElement?: ConversationAnchorNavProps['getAnchorElement']
|
|
264
|
+
/** 为 false 时不订阅滚动,返回 0;用于受控场景 */
|
|
265
|
+
enabled?: boolean
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 订阅 scroll 容器内每个锚点元素的可见性,返回"最靠近顶部且仍可见"的锚点索引。
|
|
270
|
+
* - 容器必须可滚动(通常 overflow-y: auto),且锚点元素是其后代
|
|
271
|
+
* - 每个锚点需要能通过 `getAnchorElement` 解析到 HTMLElement
|
|
272
|
+
*/
|
|
273
|
+
export function useVisibleAnchorIndex({
|
|
274
|
+
scrollContainerRef,
|
|
275
|
+
items,
|
|
276
|
+
getAnchorElement,
|
|
277
|
+
enabled = true,
|
|
278
|
+
}: UseVisibleAnchorIndexOptions): number {
|
|
279
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
280
|
+
|
|
281
|
+
// 稳定化 ids 作为 effect 依赖,避免每次 items 引用变化都重建 observer
|
|
282
|
+
const idsKey = useMemo(() => items.map((i) => i.id).join('|'), [items])
|
|
283
|
+
|
|
284
|
+
const getAnchorRef = useRef(getAnchorElement)
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
getAnchorRef.current = getAnchorElement
|
|
287
|
+
}, [getAnchorElement])
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!enabled) return
|
|
291
|
+
const container = scrollContainerRef.current
|
|
292
|
+
if (!container || items.length === 0) return
|
|
293
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
294
|
+
|
|
295
|
+
const resolve = (item: ConversationAnchorItem, index: number) => {
|
|
296
|
+
const fn = getAnchorRef.current
|
|
297
|
+
if (fn) return fn(item, index, container)
|
|
298
|
+
return container.querySelector<HTMLElement>(
|
|
299
|
+
`[data-spark-anchor="${cssEscape(item.id)}"]`,
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const anchors: Array<{ index: number; el: HTMLElement }> = []
|
|
304
|
+
items.forEach((item, index) => {
|
|
305
|
+
const el = resolve(item, index)
|
|
306
|
+
if (el) anchors.push({ index, el })
|
|
307
|
+
})
|
|
308
|
+
if (anchors.length === 0) return
|
|
309
|
+
|
|
310
|
+
const ratioByEl = new WeakMap<HTMLElement, number>()
|
|
311
|
+
|
|
312
|
+
const recompute = () => {
|
|
313
|
+
let bestIndex = anchors[0]?.index ?? 0
|
|
314
|
+
let bestRatio = -1
|
|
315
|
+
for (const { index, el } of anchors) {
|
|
316
|
+
const r = ratioByEl.get(el) ?? 0
|
|
317
|
+
if (r > bestRatio) {
|
|
318
|
+
bestRatio = r
|
|
319
|
+
bestIndex = index
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
setActiveIndex((prev) => (prev === bestIndex ? prev : bestIndex))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const observer = new IntersectionObserver(
|
|
326
|
+
(entries) => {
|
|
327
|
+
for (const e of entries) {
|
|
328
|
+
ratioByEl.set(e.target as HTMLElement, e.intersectionRatio)
|
|
329
|
+
}
|
|
330
|
+
recompute()
|
|
331
|
+
},
|
|
332
|
+
{ root: container, threshold: [0, 0.25, 0.5, 0.75, 1] },
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
anchors.forEach((a) => observer.observe(a.el))
|
|
336
|
+
|
|
337
|
+
return () => observer.disconnect()
|
|
338
|
+
// idsKey 稳定覆盖 items 内容变化;scrollContainerRef 本身稳定
|
|
339
|
+
}, [enabled, scrollContainerRef, items, idsKey])
|
|
340
|
+
|
|
341
|
+
return activeIndex
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function cssEscape(value: string): string {
|
|
345
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
346
|
+
return CSS.escape(value)
|
|
347
|
+
}
|
|
348
|
+
return value.replace(/["\\]/g, '\\$&')
|
|
349
|
+
}
|
|
@@ -20,7 +20,8 @@ export const FileAttachment = forwardRef<HTMLSpanElement, FileAttachmentProps>(
|
|
|
20
20
|
const resolvedIcon: ReactNode =
|
|
21
21
|
icon ?? (getFileIcon ? getFileIcon(lookup, ICON_CLASS) : <FileLine className={ICON_CLASS} />)
|
|
22
22
|
|
|
23
|
-
const { color
|
|
23
|
+
const { color, ...restProps } = props
|
|
24
|
+
void color
|
|
24
25
|
return (
|
|
25
26
|
<Tag
|
|
26
27
|
ref={ref}
|
|
@@ -34,27 +34,27 @@ const SETI_CHAR_MAP: Record<string, string> = {
|
|
|
34
34
|
default: '\ue023',
|
|
35
35
|
}
|
|
36
36
|
const SETI_COLOR_MAP: Record<string, string> = {
|
|
37
|
-
javascript: '
|
|
38
|
-
typescript: '
|
|
39
|
-
react: '
|
|
40
|
-
json: '
|
|
41
|
-
html: '
|
|
42
|
-
css: '
|
|
43
|
-
python: '
|
|
44
|
-
java: '
|
|
45
|
-
go: '
|
|
46
|
-
rust: '
|
|
47
|
-
php: '
|
|
48
|
-
ruby: '
|
|
49
|
-
vue: '
|
|
50
|
-
svelte: '
|
|
51
|
-
markdown: '
|
|
52
|
-
yaml: '
|
|
53
|
-
xml: '
|
|
54
|
-
shell: '
|
|
55
|
-
sass: '
|
|
56
|
-
settings: '
|
|
57
|
-
default: '
|
|
37
|
+
javascript: 'var(--color-yellow)',
|
|
38
|
+
typescript: 'var(--color-blue)',
|
|
39
|
+
react: 'var(--color-blue)',
|
|
40
|
+
json: 'var(--color-yellow)',
|
|
41
|
+
html: 'var(--color-blue)',
|
|
42
|
+
css: 'var(--color-blue)',
|
|
43
|
+
python: 'var(--color-blue)',
|
|
44
|
+
java: 'var(--color-error)',
|
|
45
|
+
go: 'var(--color-blue)',
|
|
46
|
+
rust: 'var(--color-lavender)',
|
|
47
|
+
php: 'var(--color-purple)',
|
|
48
|
+
ruby: 'var(--color-error)',
|
|
49
|
+
vue: 'var(--color-success)',
|
|
50
|
+
svelte: 'var(--color-error)',
|
|
51
|
+
markdown: 'var(--color-blue)',
|
|
52
|
+
yaml: 'var(--color-purple)',
|
|
53
|
+
xml: 'var(--color-orange)',
|
|
54
|
+
shell: 'var(--color-success)',
|
|
55
|
+
sass: 'var(--color-error)',
|
|
56
|
+
settings: 'var(--color-lavender)',
|
|
57
|
+
default: 'var(--color-lavender)',
|
|
58
58
|
}
|
|
59
59
|
const FILE_TYPE_MAP: Record<string, string> = {
|
|
60
60
|
js: 'javascript',
|
|
@@ -87,7 +87,7 @@ const markdownComponents: Components = {
|
|
|
87
87
|
if (!isBlock) {
|
|
88
88
|
return (
|
|
89
89
|
<code
|
|
90
|
-
className="relative rounded bg-fill-tertiary px-
|
|
90
|
+
className="relative rounded bg-fill-tertiary px-1 py-0.5 font-mono text-sm leading-sm font-semibold text-text"
|
|
91
91
|
{...props}
|
|
92
92
|
>
|
|
93
93
|
{children}
|
|
@@ -134,7 +134,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
|
|
|
134
134
|
<div className={cn('markdown-body text-sm leading-sm text-text', className)}>
|
|
135
135
|
<ReactMarkdown
|
|
136
136
|
remarkPlugins={[remarkGfm, remarkMath]}
|
|
137
|
-
rehypePlugins={[rehypeKatex
|
|
137
|
+
rehypePlugins={[rehypeKatex]}
|
|
138
138
|
components={markdownComponents}
|
|
139
139
|
>
|
|
140
140
|
{children}
|
|
@@ -132,6 +132,7 @@ export const QueueIndicator = memo(function QueueIndicator({
|
|
|
132
132
|
closeIcon,
|
|
133
133
|
arrowDownSIcon,
|
|
134
134
|
}: QueueIndicatorProps) {
|
|
135
|
+
void isStreaming
|
|
135
136
|
const labels = { ...DEFAULT_LABELS, ...labelsProp }
|
|
136
137
|
|
|
137
138
|
const defaultArrowUp = arrowUpIcon ?? <ArrowUpLine className={arrowUpClass} />
|
|
@@ -52,19 +52,23 @@ export function StreamingMarkdownBlock({
|
|
|
52
52
|
className,
|
|
53
53
|
}: StreamingMarkdownBlockProps) {
|
|
54
54
|
const tokens = useMemo(() => segmentForStreaming(content), [content])
|
|
55
|
-
const [
|
|
55
|
+
const [streamState, setStreamState] = useState({ content, visibleCount: isActive ? 0 : tokens.length })
|
|
56
56
|
const onStreamCompleteRef = useRef(onStreamComplete)
|
|
57
|
-
onStreamCompleteRef.current = onStreamComplete
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
onStreamCompleteRef.current = onStreamComplete
|
|
60
|
+
}, [onStreamComplete])
|
|
61
|
+
|
|
62
|
+
const visibleCount = streamState.content === content
|
|
63
|
+
? streamState.visibleCount
|
|
64
|
+
: isActive
|
|
65
|
+
? 0
|
|
66
|
+
: tokens.length
|
|
67
|
+
const visibleText = useMemo(() => tokens.slice(0, visibleCount).join(''), [tokens, visibleCount])
|
|
63
68
|
const { completed, partial } = useMemo(() => splitCompletedBlocks(visibleText), [visibleText])
|
|
64
69
|
|
|
65
70
|
useEffect(() => {
|
|
66
71
|
if (!isActive) return
|
|
67
|
-
setVisibleCount(0)
|
|
68
72
|
const len = tokens.length
|
|
69
73
|
if (len === 0) {
|
|
70
74
|
onStreamCompleteRef.current?.()
|
|
@@ -73,7 +77,7 @@ export function StreamingMarkdownBlock({
|
|
|
73
77
|
let k = 0
|
|
74
78
|
const id = setInterval(() => {
|
|
75
79
|
k = Math.min(k + TOKENS_PER_TICK, len)
|
|
76
|
-
|
|
80
|
+
setStreamState({ content, visibleCount: k })
|
|
77
81
|
if (k >= len) {
|
|
78
82
|
clearInterval(id)
|
|
79
83
|
onStreamCompleteRef.current?.()
|
|
@@ -18,6 +18,8 @@ export interface ToolInvocationCardProps {
|
|
|
18
18
|
checkboxCircleIcon?: ReactNode
|
|
19
19
|
closeCircleIcon?: ReactNode
|
|
20
20
|
arrowDownIcon?: ReactNode
|
|
21
|
+
/** @deprecated Use arrowDownIcon. */
|
|
22
|
+
arrowRightIcon?: ReactNode
|
|
21
23
|
runningText?: string
|
|
22
24
|
ranText?: string
|
|
23
25
|
failedText?: string
|
|
@@ -43,6 +45,7 @@ export const ToolInvocationCard = memo(function ToolInvocationCard({
|
|
|
43
45
|
checkboxCircleIcon,
|
|
44
46
|
closeCircleIcon,
|
|
45
47
|
arrowDownIcon,
|
|
48
|
+
arrowRightIcon,
|
|
46
49
|
runningText = 'Running',
|
|
47
50
|
ranText = 'Ran',
|
|
48
51
|
failedText = 'Failed',
|
|
@@ -62,7 +65,7 @@ export const ToolInvocationCard = memo(function ToolInvocationCard({
|
|
|
62
65
|
const defaultTime = timeIcon ?? <TimeLine className={timeClass} />
|
|
63
66
|
const defaultCheck = checkboxCircleIcon ?? <CheckboxCircleLine className={checkboxClass} />
|
|
64
67
|
const defaultCloseCircle = closeCircleIcon ?? <CloseCircleLine className={closeCircleClass} />
|
|
65
|
-
const defaultArrow = arrowDownIcon ?? <ArrowDownSLine className={arrowClass} />
|
|
68
|
+
const defaultArrow = arrowDownIcon ?? arrowRightIcon ?? <ArrowDownSLine className={arrowClass} />
|
|
66
69
|
|
|
67
70
|
return (
|
|
68
71
|
<div className={cn('flex flex-col', className)}>
|
|
@@ -27,30 +27,30 @@ export const SETI_CHAR_MAP: Record<string, string> = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
-
// Seti 图标颜色(
|
|
30
|
+
// Seti 图标颜色(tokenized,用于 Seti 字体渲染 style.color)
|
|
31
31
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
32
|
export const SETI_COLOR_MAP: Record<string, string> = {
|
|
33
|
-
javascript: '
|
|
34
|
-
typescript: '
|
|
35
|
-
react: '
|
|
36
|
-
json: '
|
|
37
|
-
html: '
|
|
38
|
-
css: '
|
|
39
|
-
python: '
|
|
40
|
-
java: '
|
|
41
|
-
go: '
|
|
42
|
-
rust: '
|
|
43
|
-
php: '
|
|
44
|
-
ruby: '
|
|
45
|
-
vue: '
|
|
46
|
-
svelte: '
|
|
47
|
-
markdown: '
|
|
48
|
-
yaml: '
|
|
49
|
-
xml: '
|
|
50
|
-
shell: '
|
|
51
|
-
sass: '
|
|
52
|
-
settings: '
|
|
53
|
-
default: '
|
|
33
|
+
javascript: 'var(--color-yellow)',
|
|
34
|
+
typescript: 'var(--color-blue)',
|
|
35
|
+
react: 'var(--color-blue)',
|
|
36
|
+
json: 'var(--color-yellow)',
|
|
37
|
+
html: 'var(--color-blue)',
|
|
38
|
+
css: 'var(--color-blue)',
|
|
39
|
+
python: 'var(--color-blue)',
|
|
40
|
+
java: 'var(--color-error)',
|
|
41
|
+
go: 'var(--color-blue)',
|
|
42
|
+
rust: 'var(--color-lavender)',
|
|
43
|
+
php: 'var(--color-purple)',
|
|
44
|
+
ruby: 'var(--color-error)',
|
|
45
|
+
vue: 'var(--color-success)',
|
|
46
|
+
svelte: 'var(--color-error)',
|
|
47
|
+
markdown: 'var(--color-blue)',
|
|
48
|
+
yaml: 'var(--color-purple)',
|
|
49
|
+
xml: 'var(--color-orange)',
|
|
50
|
+
shell: 'var(--color-success)',
|
|
51
|
+
sass: 'var(--color-error)',
|
|
52
|
+
settings: 'var(--color-lavender)',
|
|
53
|
+
default: 'var(--color-lavender)',
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// ═══════════════════════════════════════════════════════════════════════════════
|