@springbrand/message-panel 0.1.2 → 0.1.3-alpha.0

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.
Files changed (99) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/message-panel-chat-showcase.tsx +250 -0
  4. package/demo/message-panel-gallery.tsx +389 -0
  5. package/package.json +6 -16
  6. package/src/adapters/ai-sdk.ts +32 -0
  7. package/src/camel/camel-chat-messages.tsx +1423 -0
  8. package/src/camel/camel-model-select.tsx +88 -0
  9. package/src/camel/camel-plan-bar.tsx +115 -0
  10. package/src/camel/camel-prompt-input.tsx +547 -0
  11. package/src/camel/camel-tool-presentation.tsx +1270 -0
  12. package/src/camel/camel-turn.ts +316 -0
  13. package/src/camel/turn-summary-bar.tsx +81 -0
  14. package/src/chat-summary-panel.tsx +229 -0
  15. package/src/composer/chat-composer.tsx +216 -0
  16. package/src/composer/composer-attachments.tsx +138 -0
  17. package/src/composer/composer-trigger-popover.tsx +236 -0
  18. package/src/composer/composer.tsx +425 -0
  19. package/src/composer/drop-controller.ts +80 -0
  20. package/src/composer/types.ts +79 -0
  21. package/src/contracts.ts +138 -0
  22. package/src/conversation.tsx +76 -0
  23. package/src/internal/cn.ts +3 -0
  24. package/src/message-panel.tsx +368 -0
  25. package/src/message.tsx +254 -0
  26. package/src/parts/index.tsx +1149 -0
  27. package/src/parts/kind.ts +67 -0
  28. package/src/parts/plan.ts +63 -0
  29. package/src/parts/primitives.tsx +439 -0
  30. package/src/registry.ts +44 -0
  31. package/src/status.tsx +132 -0
  32. package/dist/demo/camel-chat-showcase.d.ts +0 -1
  33. package/dist/demo/camel-chat-showcase.js +0 -1007
  34. package/dist/demo/fixtures.d.ts +0 -32
  35. package/dist/demo/fixtures.js +0 -250
  36. package/dist/demo/index.js +0 -4
  37. package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
  38. package/dist/demo/message-panel-chat-showcase.js +0 -114
  39. package/dist/demo/message-panel-gallery.d.ts +0 -1
  40. package/dist/demo/message-panel-gallery.js +0 -130
  41. package/dist/src/adapters/ai-sdk.d.ts +0 -4
  42. package/dist/src/adapters/ai-sdk.js +0 -25
  43. package/dist/src/camel/camel-chat-messages.d.ts +0 -44
  44. package/dist/src/camel/camel-chat-messages.js +0 -421
  45. package/dist/src/camel/camel-model-select.d.ts +0 -13
  46. package/dist/src/camel/camel-model-select.js +0 -7
  47. package/dist/src/camel/camel-plan-bar.d.ts +0 -5
  48. package/dist/src/camel/camel-plan-bar.js +0 -24
  49. package/dist/src/camel/camel-prompt-input.d.ts +0 -32
  50. package/dist/src/camel/camel-prompt-input.js +0 -182
  51. package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
  52. package/dist/src/camel/camel-tool-presentation.js +0 -784
  53. package/dist/src/camel/camel-turn.d.ts +0 -57
  54. package/dist/src/camel/camel-turn.js +0 -203
  55. package/dist/src/camel/index.js +0 -7
  56. package/dist/src/camel/turn-summary-bar.d.ts +0 -8
  57. package/dist/src/camel/turn-summary-bar.js +0 -12
  58. package/dist/src/chat-summary-panel.d.ts +0 -22
  59. package/dist/src/chat-summary-panel.js +0 -26
  60. package/dist/src/composer/chat-composer.d.ts +0 -4
  61. package/dist/src/composer/chat-composer.js +0 -90
  62. package/dist/src/composer/composer-attachments.d.ts +0 -13
  63. package/dist/src/composer/composer-attachments.js +0 -47
  64. package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
  65. package/dist/src/composer/composer-trigger-popover.js +0 -29
  66. package/dist/src/composer/composer.d.ts +0 -15
  67. package/dist/src/composer/composer.js +0 -180
  68. package/dist/src/composer/drop-controller.d.ts +0 -8
  69. package/dist/src/composer/drop-controller.js +0 -69
  70. package/dist/src/composer/index.js +0 -6
  71. package/dist/src/composer/types.d.ts +0 -62
  72. package/dist/src/composer/types.js +0 -1
  73. package/dist/src/contracts.d.ts +0 -94
  74. package/dist/src/contracts.js +0 -1
  75. package/dist/src/conversation.d.ts +0 -13
  76. package/dist/src/conversation.js +0 -18
  77. package/dist/src/index.js +0 -12
  78. package/dist/src/internal/cn.d.ts +0 -1
  79. package/dist/src/internal/cn.js +0 -3
  80. package/dist/src/message-panel.d.ts +0 -4
  81. package/dist/src/message-panel.js +0 -144
  82. package/dist/src/message.d.ts +0 -36
  83. package/dist/src/message.js +0 -83
  84. package/dist/src/parts/index.d.ts +0 -187
  85. package/dist/src/parts/index.js +0 -332
  86. package/dist/src/parts/kind.d.ts +0 -3
  87. package/dist/src/parts/kind.js +0 -69
  88. package/dist/src/parts/plan.d.ts +0 -10
  89. package/dist/src/parts/plan.js +0 -40
  90. package/dist/src/parts/primitives.d.ts +0 -70
  91. package/dist/src/parts/primitives.js +0 -153
  92. package/dist/src/registry.d.ts +0 -4
  93. package/dist/src/registry.js +0 -31
  94. package/dist/src/status.d.ts +0 -26
  95. package/dist/src/status.js +0 -74
  96. /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
  97. /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
  98. /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
  99. /package/{dist/src/index.d.ts → src/index.ts} +0 -0
@@ -0,0 +1,292 @@
1
+ import type {
2
+ MessageAdapter,
3
+ MessagePanelStatus,
4
+ } from "../src/contracts";
5
+
6
+ export interface DemoMessage {
7
+ id: string;
8
+ role: "user" | "assistant";
9
+ parts: Array<Record<string, unknown>>;
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+
13
+ export const demoMessageAdapter: MessageAdapter<DemoMessage> = {
14
+ getId: (message) => message.id,
15
+ getRole: (message) => message.role,
16
+ getParts: (message) => message.parts,
17
+ getMetadata: (message) => message.metadata,
18
+ getText: (message) =>
19
+ message.parts
20
+ .filter((part) => part.type === "text")
21
+ .map((part) => String(part.text ?? ""))
22
+ .join("\n"),
23
+ isPartRenderable: () => true,
24
+ };
25
+
26
+ const demoImage =
27
+ "data:image/svg+xml;charset=utf-8," +
28
+ encodeURIComponent(
29
+ '<svg xmlns="http://www.w3.org/2000/svg" width="900" height="520"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop stop-color="#39274f"/><stop offset=".55" stop-color="#e07042"/><stop offset="1" stop-color="#f6c274"/></linearGradient></defs><rect width="900" height="520" fill="url(#g)"/><circle cx="700" cy="125" r="68" fill="#ffe4ad"/><path d="M0 410 190 210 360 410 540 170 900 430V520H0Z" fill="#191e2c"/><path d="M270 435V320l80-58 82 58v115Z" fill="#69432e"/><rect x="324" y="348" width="48" height="52" fill="#ffd47d"/></svg>',
30
+ );
31
+
32
+ export const allPartsMessage: DemoMessage = {
33
+ id: "assistant-all-parts",
34
+ role: "assistant",
35
+ parts: [
36
+ {
37
+ type: "text",
38
+ text: "## Message Panel\n\n支持 **GFM Markdown**、真实流式状态与富组件叙事流。",
39
+ },
40
+ {
41
+ type: "reasoning",
42
+ label: "Thought for 3s",
43
+ detail: "先理解目标,再安排步骤和并行任务。",
44
+ },
45
+ {
46
+ type: "tool",
47
+ icon: "web",
48
+ label: "Searched the web",
49
+ meta: "2 results",
50
+ status: "output-available",
51
+ results: [
52
+ { title: "AI interface patterns", domain: "example.com" },
53
+ { title: "Streaming UI guide", domain: "docs.example.com" },
54
+ ],
55
+ },
56
+ {
57
+ type: "error",
58
+ title: "一项后台任务已中断",
59
+ body: "连接恢复后可以安全重试,不会留下无限加载状态。",
60
+ },
61
+ {
62
+ type: "image",
63
+ src: demoImage,
64
+ alt: "雪山日落与木屋的示意图",
65
+ },
66
+ {
67
+ type: "chart",
68
+ title: "Keyword opportunities",
69
+ unit: "",
70
+ expandable: true,
71
+ bars: [
72
+ { label: "Agent UI", value: 96 },
73
+ { label: "RAG", value: 78 },
74
+ { label: "Tools", value: 65 },
75
+ { label: "Memory", value: 54 },
76
+ ],
77
+ },
78
+ {
79
+ type: "agent_card",
80
+ title: "Your agent is ready",
81
+ agentName: "Research Analyst",
82
+ agentDesc: "持续调研市场并把结果汇总成结构化报告。",
83
+ },
84
+ {
85
+ type: "plan",
86
+ title: "任务计划",
87
+ steps: [
88
+ { text: "确认目标与边界", status: "done" },
89
+ { text: "检索并验证资料", status: "in_progress" },
90
+ { text: "交付报告", status: "pending" },
91
+ ],
92
+ },
93
+ {
94
+ type: "schedule",
95
+ title: "每周发送竞品报告",
96
+ cadence: "Every Monday at 9:00 AM",
97
+ nextRun: "Mon, Aug 3 · Asia/Shanghai",
98
+ },
99
+ {
100
+ type: "parallel",
101
+ label: "Parallel research",
102
+ tools: [
103
+ { icon: "web", label: "市场调研", status: "done", meta: "8 sources" },
104
+ { icon: "code", label: "数据分析", status: "running" },
105
+ { icon: "file", label: "汇总报告", status: "done" },
106
+ ],
107
+ },
108
+ {
109
+ type: "subagents",
110
+ agents: [
111
+ {
112
+ name: "Research Agent",
113
+ tint: "#5577cc",
114
+ summary: "完成 8 个来源的交叉验证。",
115
+ status: "done",
116
+ },
117
+ {
118
+ name: "Data Agent",
119
+ tint: "#8f61c7",
120
+ summary: "正在计算趋势指标。",
121
+ status: "running",
122
+ },
123
+ ],
124
+ },
125
+ {
126
+ type: "confirmation",
127
+ id: "demo-confirmation",
128
+ title: "创建每周竞品 Agent?",
129
+ body: "将按上述计划在每周一运行。",
130
+ confirmLabel: "创建 Agent",
131
+ cancelLabel: "暂不",
132
+ },
133
+ {
134
+ type: "file",
135
+ name: "competitor-report.md",
136
+ kind: "doc",
137
+ note: "Markdown · 14 KB",
138
+ },
139
+ {
140
+ type: "dashboard",
141
+ title: "Agent 运行概览",
142
+ tiles: [
143
+ { label: "完成任务", value: "42" },
144
+ { label: "成功率", value: "98%" },
145
+ { label: "节省时间", value: "16h" },
146
+ ],
147
+ cards: [
148
+ { icon: "🔎", title: "Research", body: "8 个来源完成验证" },
149
+ { icon: "📊", title: "Analysis", body: "3 个趋势值得关注" },
150
+ { icon: "📝", title: "Report", body: "报告已生成" },
151
+ ],
152
+ badges: ["实时流式", "可恢复", "工作区联动"],
153
+ fileNote: {
154
+ title: "文件已生成",
155
+ body: "报告已保存到 Workspace。",
156
+ },
157
+ table: {
158
+ columns: ["能力", "状态"],
159
+ rows: [
160
+ ["Message", "Ready"],
161
+ ["Composer", "Ready"],
162
+ ],
163
+ },
164
+ },
165
+ {
166
+ type: "ask_user",
167
+ questions: [
168
+ {
169
+ prompt: "报告通过什么渠道发送?",
170
+ kind: "single",
171
+ options: ["Email", "Slack", "Both"],
172
+ },
173
+ {
174
+ prompt: "需要包含哪些内容?",
175
+ kind: "multi",
176
+ options: ["定价", "功能", "动态"],
177
+ },
178
+ ],
179
+ freeTextPlaceholder: "补充竞争对手名称…",
180
+ },
181
+ {
182
+ type: "suggestions",
183
+ items: ["立即运行一次", "加入 Slack 通知", "编辑跟踪名单"],
184
+ },
185
+ ],
186
+ };
187
+
188
+ export const basicMessages: DemoMessage[] = [
189
+ {
190
+ id: "demo-user",
191
+ role: "user",
192
+ parts: [{ type: "text", text: "帮我制作一份竞品周报。" }],
193
+ },
194
+ {
195
+ id: "demo-assistant",
196
+ role: "assistant",
197
+ parts: [{ type: "text", text: "好的,我会先做计划,再开始调研。" }],
198
+ },
199
+ ];
200
+
201
+ export const panelScenarios: Array<{
202
+ id: string;
203
+ title: string;
204
+ messages: DemoMessage[];
205
+ status: MessagePanelStatus;
206
+ flags?: {
207
+ isHydrating?: boolean;
208
+ isRecovering?: boolean;
209
+ isServerStreaming?: boolean;
210
+ };
211
+ error?: Error;
212
+ }> = [
213
+ { id: "empty", title: "Empty", messages: [], status: "ready" },
214
+ {
215
+ id: "hydrating",
216
+ title: "Hydrating",
217
+ messages: basicMessages,
218
+ status: "ready",
219
+ flags: { isHydrating: true },
220
+ },
221
+ {
222
+ id: "thinking",
223
+ title: "Thinking / Submitted",
224
+ messages: basicMessages.slice(0, 1),
225
+ status: "submitted",
226
+ },
227
+ {
228
+ id: "streaming",
229
+ title: "Streaming / Playing",
230
+ messages: [
231
+ basicMessages[0],
232
+ {
233
+ id: "streaming-assistant",
234
+ role: "assistant",
235
+ parts: [{ type: "text", text: "正在检索并交叉验证资料…" }],
236
+ },
237
+ ],
238
+ status: "streaming",
239
+ },
240
+ {
241
+ id: "completed",
242
+ title: "Completed / Ready",
243
+ messages: basicMessages,
244
+ status: "ready",
245
+ },
246
+ {
247
+ id: "error",
248
+ title: "Error / Interrupted",
249
+ messages: basicMessages,
250
+ status: "error",
251
+ error: new Error("网络连接已中断,可安全重试。"),
252
+ },
253
+ {
254
+ id: "recovering",
255
+ title: "Recovering / Background",
256
+ messages: basicMessages,
257
+ status: "ready",
258
+ flags: { isRecovering: true, isServerStreaming: true },
259
+ },
260
+ ];
261
+
262
+ export interface DemoAttachment {
263
+ id: string;
264
+ filename: string;
265
+ status: "uploading" | "ready" | "error";
266
+ mediaType: string;
267
+ size: number;
268
+ }
269
+
270
+ export const readyAttachment: DemoAttachment = {
271
+ id: "ready-file",
272
+ filename: "brief.pdf",
273
+ status: "ready",
274
+ mediaType: "application/pdf",
275
+ size: 21840,
276
+ };
277
+
278
+ export const uploadingAttachment: DemoAttachment = {
279
+ ...readyAttachment,
280
+ id: "uploading-file",
281
+ filename: "research.csv",
282
+ status: "uploading",
283
+ mediaType: "text/csv",
284
+ };
285
+
286
+ export const errorAttachment: DemoAttachment = {
287
+ ...readyAttachment,
288
+ id: "error-file",
289
+ filename: "broken.zip",
290
+ status: "error",
291
+ mediaType: "application/zip",
292
+ };
@@ -0,0 +1,250 @@
1
+ import { useMemo, useState } from "react";
2
+ import { MessagePanel, type MessagePanelStatus } from "../src";
3
+ import { Composer, DropOverlay } from "../src/composer";
4
+ import {
5
+ allPartsMessage,
6
+ basicMessages,
7
+ demoMessageAdapter,
8
+ errorAttachment,
9
+ panelScenarios,
10
+ readyAttachment,
11
+ uploadingAttachment,
12
+ type DemoAttachment,
13
+ } from "./fixtures";
14
+
15
+ const noOp = () => undefined;
16
+
17
+ interface ComposerScenario {
18
+ id: string;
19
+ title: string;
20
+ value?: string;
21
+ attachments?: DemoAttachment[];
22
+ status: MessagePanelStatus;
23
+ overlay?: boolean;
24
+ }
25
+
26
+ const composerScenarios: ComposerScenario[] = [
27
+ { id: "empty", title: "Empty", status: "ready" },
28
+ {
29
+ id: "drafting",
30
+ title: "Drafting",
31
+ value: "为这份数据生成报告",
32
+ status: "ready",
33
+ },
34
+ { id: "drag-active", title: "Drag active", status: "ready", overlay: true },
35
+ {
36
+ id: "uploading",
37
+ title: "Uploading",
38
+ attachments: [uploadingAttachment],
39
+ status: "ready",
40
+ },
41
+ {
42
+ id: "attachment-error",
43
+ title: "Attachment error",
44
+ value: "不要丢掉这份草稿",
45
+ attachments: [errorAttachment],
46
+ status: "ready",
47
+ },
48
+ {
49
+ id: "ready-with-attachments",
50
+ title: "Ready with attachments",
51
+ attachments: [readyAttachment],
52
+ status: "ready",
53
+ },
54
+ {
55
+ id: "submitted-streaming",
56
+ title: "Submitted / Streaming",
57
+ value: "正在发送…",
58
+ status: "streaming",
59
+ },
60
+ ];
61
+
62
+ const controlStyle = {
63
+ minHeight: 34,
64
+ padding: "6px 10px",
65
+ border: "1px solid var(--border)",
66
+ borderRadius: 9,
67
+ color: "var(--foreground)",
68
+ background: "var(--card)",
69
+ font: "inherit",
70
+ fontSize: 12,
71
+ } as const;
72
+
73
+ export function MessagePanelChatShowcase() {
74
+ const [theme, setTheme] = useState<"light" | "dark">("dark");
75
+ const [panelScenarioId, setPanelScenarioId] = useState("completed");
76
+ const [composerScenarioId, setComposerScenarioId] = useState("empty");
77
+ const panelScenario =
78
+ panelScenarios.find((scenario) => scenario.id === panelScenarioId) ??
79
+ panelScenarios[4];
80
+ const composerScenario =
81
+ composerScenarios.find(
82
+ (scenario) => scenario.id === composerScenarioId,
83
+ ) ?? composerScenarios[0];
84
+ const messages = useMemo(
85
+ () =>
86
+ panelScenario.id === "completed"
87
+ ? [basicMessages[0], allPartsMessage]
88
+ : panelScenario.messages,
89
+ [panelScenario],
90
+ );
91
+
92
+ return (
93
+ <main
94
+ className={theme === "dark" ? "dark" : ""}
95
+ data-message-panel-chat-showcase=""
96
+ style={{
97
+ display: "grid",
98
+ height: "100dvh",
99
+ minHeight: 0,
100
+ gridTemplateRows: "auto minmax(0, 1fr) auto",
101
+ overflow: "hidden",
102
+ color: "var(--foreground)",
103
+ background: "var(--background)",
104
+ fontFamily: "var(--font-sans)",
105
+ }}
106
+ >
107
+ <header
108
+ style={{
109
+ zIndex: 30,
110
+ display: "flex",
111
+ minWidth: 0,
112
+ flexWrap: "wrap",
113
+ alignItems: "center",
114
+ gap: 10,
115
+ padding: "10px clamp(12px, 2vw, 24px)",
116
+ borderBottom: "1px solid var(--border)",
117
+ background: "var(--background)",
118
+ }}
119
+ >
120
+ <a
121
+ href="/"
122
+ style={{
123
+ color: "var(--muted-foreground)",
124
+ fontSize: 13,
125
+ textDecoration: "none",
126
+ }}
127
+ >
128
+ ← 返回系统
129
+ </a>
130
+ <div style={{ minWidth: 190, marginRight: "auto" }}>
131
+ <h1 style={{ margin: 0, fontSize: 14 }}>
132
+ UI 全场景验收 Chat
133
+ </h1>
134
+ <small style={{ color: "var(--muted-foreground)" }}>
135
+ 16 类组件 · 7 类生命周期 · 7 类输入状态
136
+ </small>
137
+ </div>
138
+ <label
139
+ style={{
140
+ display: "flex",
141
+ alignItems: "center",
142
+ gap: 7,
143
+ color: "var(--muted-foreground)",
144
+ fontSize: 12,
145
+ }}
146
+ >
147
+ 消息状态
148
+ <select
149
+ aria-label="消息状态"
150
+ value={panelScenarioId}
151
+ onChange={(event) => setPanelScenarioId(event.currentTarget.value)}
152
+ style={controlStyle}
153
+ >
154
+ {panelScenarios.map((scenario) => (
155
+ <option key={scenario.id} value={scenario.id}>
156
+ {scenario.title}
157
+ </option>
158
+ ))}
159
+ </select>
160
+ </label>
161
+ <label
162
+ style={{
163
+ display: "flex",
164
+ alignItems: "center",
165
+ gap: 7,
166
+ color: "var(--muted-foreground)",
167
+ fontSize: 12,
168
+ }}
169
+ >
170
+ 输入状态
171
+ <select
172
+ aria-label="输入状态"
173
+ value={composerScenarioId}
174
+ onChange={(event) =>
175
+ setComposerScenarioId(event.currentTarget.value)
176
+ }
177
+ style={controlStyle}
178
+ >
179
+ {composerScenarios.map((scenario) => (
180
+ <option key={scenario.id} value={scenario.id}>
181
+ {scenario.title}
182
+ </option>
183
+ ))}
184
+ </select>
185
+ </label>
186
+ <button
187
+ type="button"
188
+ onClick={() =>
189
+ setTheme((current) => (current === "dark" ? "light" : "dark"))
190
+ }
191
+ style={controlStyle}
192
+ >
193
+ {theme === "dark" ? "浅色" : "深色"}
194
+ </button>
195
+ </header>
196
+
197
+ <MessagePanel
198
+ messages={messages}
199
+ status={panelScenario.status}
200
+ isHydrating={panelScenario.flags?.isHydrating}
201
+ isRecovering={panelScenario.flags?.isRecovering}
202
+ isServerStreaming={panelScenario.flags?.isServerStreaming}
203
+ error={panelScenario.error}
204
+ adapter={demoMessageAdapter}
205
+ actions={{
206
+ onSuggestion: noOp,
207
+ onAnswer: noOp,
208
+ onApprove: noOp,
209
+ onPreview: noOp,
210
+ onDownload: noOp,
211
+ onRetry: noOp,
212
+ }}
213
+ />
214
+
215
+ <footer
216
+ style={{
217
+ position: "relative",
218
+ zIndex: 20,
219
+ padding: "8px clamp(14px, 4vw, 32px) 18px",
220
+ background:
221
+ "linear-gradient(to top, var(--background) 76%, transparent)",
222
+ }}
223
+ >
224
+ <div
225
+ data-composer-showcase={composerScenario.id}
226
+ style={{
227
+ position: "relative",
228
+ width: "min(100%, 780px)",
229
+ marginInline: "auto",
230
+ }}
231
+ >
232
+ <Composer
233
+ key={composerScenario.id}
234
+ defaultValue={composerScenario.value}
235
+ status={composerScenario.status}
236
+ attachments={composerScenario.attachments}
237
+ onSubmit={noOp}
238
+ onStop={noOp}
239
+ onFilesAdded={noOp}
240
+ onAttachmentRemove={noOp}
241
+ onAttachmentRetry={noOp}
242
+ onAttachmentReorder={noOp}
243
+ placeholder="这是验收夹具,不会发送或保存数据"
244
+ />
245
+ {composerScenario.overlay && <DropOverlay />}
246
+ </div>
247
+ </footer>
248
+ </main>
249
+ );
250
+ }