@polderlabs/bizar 4.4.13 → 4.5.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 (90) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
  3. package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
  4. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
  6. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
  7. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
  8. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  12. package/bizar-dash/skills/bizar/SKILL.md +96 -0
  13. package/bizar-dash/skills/chat/SKILL.md +74 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +75 -0
  15. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  16. package/bizar-dash/skills/obsidian/SKILL.md +55 -0
  17. package/bizar-dash/skills/providers/SKILL.md +75 -0
  18. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  19. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  20. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  21. package/bizar-dash/skills/usage/SKILL.md +62 -0
  22. package/bizar-dash/src/server/api.mjs +12 -0
  23. package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
  24. package/bizar-dash/src/server/memory-store.mjs +38 -0
  25. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  26. package/bizar-dash/src/server/minimax.mjs +196 -5
  27. package/bizar-dash/src/server/providers-store.mjs +956 -0
  28. package/bizar-dash/src/server/routes/config.mjs +52 -1
  29. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  30. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  31. package/bizar-dash/src/server/routes/memory.mjs +241 -1
  32. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  33. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  34. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  35. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  36. package/bizar-dash/src/server/routes/update.mjs +340 -0
  37. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  38. package/bizar-dash/src/server/serve-info.mjs +135 -4
  39. package/bizar-dash/src/server/server.mjs +4 -0
  40. package/bizar-dash/src/server/skills-store.mjs +152 -262
  41. package/bizar-dash/src/web/App.tsx +118 -29
  42. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  43. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  44. package/bizar-dash/src/web/components/Topbar.tsx +0 -1
  45. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  46. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  47. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  48. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  49. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  50. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  51. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  52. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  53. package/bizar-dash/src/web/lib/api.ts +43 -0
  54. package/bizar-dash/src/web/main.tsx +1 -0
  55. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  56. package/bizar-dash/src/web/styles/chat.css +135 -1
  57. package/bizar-dash/src/web/styles/main.css +46 -0
  58. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  59. package/bizar-dash/src/web/styles/settings.css +418 -0
  60. package/bizar-dash/src/web/styles/skills.css +302 -0
  61. package/bizar-dash/src/web/styles/tasks.css +288 -0
  62. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  63. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  64. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  65. package/bizar-dash/src/web/views/Settings.tsx +6 -0
  66. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  67. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  68. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  69. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  70. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  71. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  72. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  73. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  74. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  75. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  76. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  77. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  78. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  79. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  80. package/bizar-dash/tests/update-check.test.mjs +127 -0
  81. package/bizar-dash/tests/update-run.test.mjs +266 -0
  82. package/cli/bin.mjs +82 -1
  83. package/cli/provision.mjs +118 -4
  84. package/config/agents/_shared/SKILLS.md +109 -0
  85. package/package.json +1 -1
  86. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  87. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  88. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  89. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  90. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -0,0 +1,250 @@
1
+ // src/web/components/UsageChart.tsx
2
+ //
3
+ // Hand-rolled SVG chart. No external chart library — keeps bundle small.
4
+ //
5
+ // Props:
6
+ // series: { labels: string[]; requests: number[]; tokens: number[] }
7
+ // height?: number (default 280)
8
+ // onHover?: (index: number | null, x: number, y: number) => void
9
+
10
+ import { useCallback, useRef, useState } from 'react';
11
+ import { cn } from '../lib/utils';
12
+
13
+ type Props = {
14
+ series: { labels: string[]; requests: number[]; tokens: number[] };
15
+ height?: number;
16
+ onHover?: (index: number | null, x: number, y: number) => void;
17
+ className?: string;
18
+ };
19
+
20
+ const PADDING = { top: 16, right: 20, bottom: 40, left: 52 };
21
+
22
+ export function UsageChart({ series, height = 280, onHover, className }: Props) {
23
+ const containerRef = useRef<HTMLDivElement>(null);
24
+ const [tooltip, setTooltip] = useState<{ index: number; x: number; y: number } | null>(null);
25
+
26
+ const { labels, requests, tokens } = series;
27
+ const n = labels.length;
28
+ if (n === 0) {
29
+ return (
30
+ <div className={cn('usage-chart-empty', className)} style={{ height }}>
31
+ <span>No data for this range</span>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ const innerW = 600 - PADDING.left - PADDING.right;
37
+ const innerH = height - PADDING.top - PADDING.bottom;
38
+
39
+ const maxReq = Math.max(...requests, 1);
40
+ const maxTok = Math.max(...tokens, 1);
41
+ // Two separate Y scales (left = requests, right = tokens)
42
+ const barW = Math.max(4, Math.min(24, Math.floor(innerW / n) - 4));
43
+ const gap = (innerW - barW * n) / (n + 1);
44
+
45
+ function barX(i: number) {
46
+ return PADDING.left + gap + i * (barW + gap);
47
+ }
48
+
49
+ function barH(v: number) {
50
+ return (v / maxReq) * innerH * 0.75;
51
+ }
52
+
53
+ function lineY(v: number) {
54
+ return PADDING.top + innerH - (v / maxTok) * innerH * 0.75;
55
+ }
56
+
57
+ // Build polyline points for the token line.
58
+ const linePoints = tokens
59
+ .map((v, i) => `${barX(i) + barW / 2},${lineY(v)}`)
60
+ .join(' ');
61
+
62
+ // Y-axis ticks (left, requests).
63
+ const leftTicks = [0, 0.25, 0.5, 0.75, 1].map(t => ({
64
+ y: PADDING.top + innerH - t * innerH * 0.75,
65
+ label: t === 0 ? '0' : `${Math.round(maxReq * t).toLocaleString()}`,
66
+ }));
67
+
68
+ // Y-axis ticks (right, tokens).
69
+ const rightTicks = [0, 0.25, 0.5, 0.75, 1].map(t => ({
70
+ y: PADDING.top + innerH - t * innerH * 0.75,
71
+ label: t === 0 ? '0' : `${Math.round(maxTok * t / 1000)}k`,
72
+ }));
73
+
74
+ // X-axis labels (show at most 7 to avoid crowding).
75
+ const labelStep = Math.max(1, Math.floor(n / 7));
76
+ const xLabels = labels.map((l, i) => ({ i, l, x: barX(i) + barW / 2 })).filter((_, i) => i % labelStep === 0);
77
+
78
+ const handleMouseMove = useCallback(
79
+ (e: React.MouseEvent<SVGSVGElement>) => {
80
+ if (!containerRef.current) return;
81
+ const rect = containerRef.current.getBoundingClientRect();
82
+ const svgX = (e.clientX - rect.left) * (600 / rect.width);
83
+ const svgY = (e.clientY - rect.top) * (height / rect.height);
84
+ // Find closest bar.
85
+ const relX = svgX - PADDING.left;
86
+ const i = Math.round((relX - gap / 2) / (barW + gap));
87
+ const clamped = Math.max(0, Math.min(n - 1, i));
88
+ setTooltip({ index: clamped, x: e.clientX - rect.left, y: e.clientY - rect.top });
89
+ onHover?.(clamped, svgX, svgY);
90
+ },
91
+ [n, barW, gap, onHover, height],
92
+ );
93
+
94
+ const handleMouseLeave = useCallback(() => {
95
+ setTooltip(null);
96
+ onHover?.(null, 0, 0);
97
+ }, [onHover]);
98
+
99
+ const tip = tooltip;
100
+
101
+ return (
102
+ <div ref={containerRef} className={cn('usage-chart', className)} style={{ position: 'relative' }}>
103
+ <svg
104
+ viewBox={`0 0 600 ${height}`}
105
+ width="100%"
106
+ height={height}
107
+ style={{ display: 'block', overflow: 'visible' }}
108
+ onMouseMove={handleMouseMove}
109
+ onMouseLeave={handleMouseLeave}
110
+ >
111
+ {/* Grid lines */}
112
+ {leftTicks.map((tick, i) => (
113
+ <line
114
+ key={`grid-${i}`}
115
+ x1={PADDING.left}
116
+ y1={tick.y}
117
+ x2={600 - PADDING.right}
118
+ y2={tick.y}
119
+ stroke="var(--border)"
120
+ strokeWidth={1}
121
+ strokeDasharray="4,3"
122
+ opacity={0.5}
123
+ />
124
+ ))}
125
+
126
+ {/* Left Y axis — requests */}
127
+ {leftTicks.map((tick, i) => (
128
+ <text
129
+ key={`ly-${i}`}
130
+ x={PADDING.left - 6}
131
+ y={tick.y + 4}
132
+ textAnchor="end"
133
+ fontSize={10}
134
+ fill="var(--text-muted)"
135
+ fontFamily="var(--font-mono, ui-monospace, monospace)"
136
+ >
137
+ {tick.label}
138
+ </text>
139
+ ))}
140
+
141
+ {/* Right Y axis — tokens */}
142
+ {rightTicks.map((tick, i) => (
143
+ <text
144
+ key={`ry-${i}`}
145
+ x={600 - PADDING.right + 6}
146
+ y={tick.y + 4}
147
+ textAnchor="start"
148
+ fontSize={10}
149
+ fill="var(--text-muted)"
150
+ fontFamily="var(--font-mono, ui-monospace, monospace)"
151
+ >
152
+ {tick.label}
153
+ </text>
154
+ ))}
155
+
156
+ {/* Bars — requests */}
157
+ {requests.map((v, i) => {
158
+ const bh = barH(v);
159
+ const bx = barX(i);
160
+ const by = PADDING.top + innerH - bh;
161
+ return (
162
+ <rect
163
+ key={`bar-${i}`}
164
+ x={bx}
165
+ y={by}
166
+ width={barW}
167
+ height={bh}
168
+ rx={2}
169
+ fill={tip?.index === i ? 'var(--accent)' : 'var(--accent)'}
170
+ opacity={tip?.index === i ? 1 : 0.75}
171
+ style={{ transition: 'opacity 120ms ease' }}
172
+ />
173
+ );
174
+ })}
175
+
176
+ {/* Token line */}
177
+ <polyline
178
+ points={linePoints}
179
+ fill="none"
180
+ stroke="var(--warning, #d29922)"
181
+ strokeWidth={2}
182
+ strokeLinejoin="round"
183
+ strokeLinecap="round"
184
+ opacity={0.9}
185
+ />
186
+
187
+ {/* Token dots */}
188
+ {tokens.map((v, i) => (
189
+ <circle
190
+ key={`dot-${i}`}
191
+ cx={barX(i) + barW / 2}
192
+ cy={lineY(v)}
193
+ r={tip?.index === i ? 5 : 3}
194
+ fill="var(--warning, #d29922)"
195
+ opacity={tip?.index === i ? 1 : 0.7}
196
+ style={{ transition: 'r 120ms ease' }}
197
+ />
198
+ ))}
199
+
200
+ {/* X axis labels */}
201
+ {xLabels.map(({ i, l, x }) => (
202
+ <text
203
+ key={`xl-${i}`}
204
+ x={x}
205
+ y={PADDING.top + innerH + 18}
206
+ textAnchor="middle"
207
+ fontSize={10}
208
+ fill="var(--text-muted)"
209
+ fontFamily="var(--font-mono, ui-monospace, monospace)"
210
+ >
211
+ {l}
212
+ </text>
213
+ ))}
214
+
215
+ {/* X axis line */}
216
+ <line
217
+ x1={PADDING.left}
218
+ y1={PADDING.top + innerH}
219
+ x2={600 - PADDING.right}
220
+ y2={PADDING.top + innerH}
221
+ stroke="var(--border)"
222
+ strokeWidth={1}
223
+ />
224
+ </svg>
225
+
226
+ {/* Floating tooltip */}
227
+ {tip !== null && (
228
+ <div
229
+ className="usage-chart-tooltip"
230
+ style={{
231
+ left: Math.min(tip.x + 12, 600 - 140),
232
+ top: tip.y - 60,
233
+ }}
234
+ >
235
+ <div className="usage-tooltip-date">{labels[tip.index]}</div>
236
+ <div className="usage-tooltip-row">
237
+ <span className="usage-tooltip-dot" style={{ background: 'var(--accent)' }} />
238
+ <span>Requests:</span>
239
+ <strong>{requests[tip.index].toLocaleString()}</strong>
240
+ </div>
241
+ <div className="usage-tooltip-row">
242
+ <span className="usage-tooltip-dot" style={{ background: 'var(--warning, #d29922)' }} />
243
+ <span>Tokens:</span>
244
+ <strong>{tokens[tip.index].toLocaleString()}</strong>
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+ );
250
+ }
@@ -0,0 +1,90 @@
1
+ // src/web/components/UsageTable.tsx
2
+ //
3
+ // Sortable compact table for per-model usage breakdown.
4
+ //
5
+ // Props:
6
+ // rows: PerModelRow[]
7
+ // sortKey: string
8
+ // sortDir: 'asc' | 'desc'
9
+ // onSort: (key: string) => void
10
+
11
+ import { cn } from '../lib/utils';
12
+
13
+ export type PerModelRow = {
14
+ providerId: string;
15
+ modelId: string;
16
+ requests: number;
17
+ totalTokens: number;
18
+ promptTokens: number;
19
+ completionTokens: number;
20
+ errors: number;
21
+ avgLatencyMs: number;
22
+ };
23
+
24
+ type Props = {
25
+ rows: PerModelRow[];
26
+ sortKey: string;
27
+ sortDir: 'asc' | 'desc';
28
+ onSort: (key: string) => void;
29
+ };
30
+
31
+ function SortHeader({ label, col, sortKey, sortDir, onSort }: {
32
+ label: string; col: string; sortKey: string; sortDir: 'asc' | 'desc'; onSort: (k: string) => void;
33
+ }) {
34
+ const active = sortKey === col;
35
+ return (
36
+ <th
37
+ className={cn('usage-sort-header', active && 'is-active')}
38
+ onClick={() => onSort(col)}
39
+ aria-sort={active ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
40
+ >
41
+ <span>{label}</span>
42
+ <span className="usage-sort-icon">
43
+ {active ? (sortDir === 'asc' ? ' ↑' : ' ↓') : ' ↕'}
44
+ </span>
45
+ </th>
46
+ );
47
+ }
48
+
49
+ export function UsageTable({ rows, sortKey, sortDir, onSort }: Props) {
50
+ return (
51
+ <div className="usage-table-wrap">
52
+ <table className="usage-table">
53
+ <thead>
54
+ <tr>
55
+ <SortHeader label="Model" col="modelId" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
56
+ <SortHeader label="Requests" col="requests" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
57
+ <SortHeader label="Prompt Tok" col="promptTokens" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
58
+ <SortHeader label="Compl Tok" col="completionTokens" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
59
+ <SortHeader label="Total Tok" col="totalTokens" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
60
+ <SortHeader label="Avg Latency" col="avgLatencyMs" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
61
+ <SortHeader label="Errors" col="errors" sortKey={sortKey} sortDir={sortDir} onSort={onSort} />
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ {rows.length === 0 ? (
66
+ <tr>
67
+ <td colSpan={7} className="usage-table-empty">No data for this range</td>
68
+ </tr>
69
+ ) : (
70
+ rows.map((row) => (
71
+ <tr key={`${row.providerId}::${row.modelId}`}>
72
+ <td className="usage-table-model">
73
+ <code>{row.modelId}</code>
74
+ </td>
75
+ <td className="mono">{row.requests.toLocaleString()}</td>
76
+ <td className="mono">{row.promptTokens.toLocaleString()}</td>
77
+ <td className="mono">{row.completionTokens.toLocaleString()}</td>
78
+ <td className="mono">{row.totalTokens.toLocaleString()}</td>
79
+ <td className="mono">{row.avgLatencyMs}ms</td>
80
+ <td className={cn('mono', row.errors > 0 && 'is-err')}>
81
+ {row.errors > 0 ? row.errors : '—'}
82
+ </td>
83
+ </tr>
84
+ ))
85
+ )}
86
+ </tbody>
87
+ </table>
88
+ </div>
89
+ );
90
+ }
@@ -1,18 +1,9 @@
1
- // src/components/chat/ChatComposer.tsx — bottom composer pill (v3.22 design).
1
+ // src/components/chat/ChatComposer.tsx — bottom composer pill (v4.2.5).
2
2
  //
3
- // Replaces FloatingComposer. Visual layout follows the design HTML:
4
- //
5
- // [agent chip] [model badge] [──── textarea ────] [📎] [✨] [➤]
6
- //
7
- // All wrapped in a single pill (chat-composer-pill) with rounded ends
8
- // and a focus-within accent border. The legacy Composer component
9
- // already provides the textarea, agent chip menu, attachment tags,
10
- // and slash-command suggestion dropdown — the redesign simply nests
11
- // the Composer inside the new pill chrome and adds the hint row
12
- // (⏎ / ⇧⏎ / /) underneath.
13
- //
14
- // Props match the original FloatingComposer EXACTLY (so Chat.tsx +
15
- // MobileChat.tsx don't need any changes).
3
+ // v4.2.5 small refinement: the pill's border picks up the
4
+ // activeSource color (opencode → accent border; bizar → neutral),
5
+ // so the user has visual confirmation that their next message will
6
+ // land in the right backend.
16
7
 
17
8
  import { useState } from 'react';
18
9
  import { Composer } from './Composer';
@@ -32,31 +23,29 @@ interface Props {
32
23
  onPickSuggestion: (cmd: string) => void;
33
24
  agents: Array<{ name: string }>;
34
25
  onAttach: () => void;
35
- /** Backward-compat used to be page-level panel toggles. Ignored in
36
- * the redesign (rail + info are always visible). */
26
+ /** Which stream will receive the next message. */
27
+ activeSource?: 'bizar' | 'opencode' | null;
28
+ /** Backward-compat — used to be page-level panel toggles. Ignored. */
37
29
  sessionsOpen?: boolean;
38
30
  /** Backward-compat — see sessionsOpen. */
39
31
  infoOpen?: boolean;
40
32
  }
41
33
 
42
34
  export function ChatComposer(props: Props) {
43
- const { text, sending, onSend } = props;
35
+ const { text, sending, onSend, activeSource } = props;
44
36
  const [takingOff, setTakingOff] = useState(false);
45
37
 
46
- // Wrap onSend with a brief takeoff animation — a small CSS hook on
47
- // the wrapper pill. We don't animate a specific element here; the
48
- // pill itself gets the brief `takeoff` class so the send button
49
- // (the last child of Composer) gets the animation if the design
50
- // wants it.
51
38
  const handleSend = () => {
52
- if (!text.trim()) return;
39
+ if (!text.trim() || sending) return;
53
40
  setTakingOff(true);
54
41
  onSend();
55
42
  window.setTimeout(() => setTakingOff(false), 320);
56
43
  };
57
44
 
58
45
  return (
59
- <div className="chat-composer-wrap">
46
+ <div
47
+ className={`chat-composer-wrap chat-composer-source-${activeSource ?? 'none'}`}
48
+ >
60
49
  <div className={`chat-composer-pill${takingOff ? ' takeoff' : ''}`}>
61
50
  <Composer {...props} onSend={handleSend} />
62
51
  </div>
@@ -70,7 +59,14 @@ export function ChatComposer(props: Props) {
70
59
  <span>
71
60
  <kbd>/</kbd> commands
72
61
  </span>
62
+ {activeSource === 'opencode' && (
63
+ <span className="chat-composer-source-hint">→ opencode</span>
64
+ )}
65
+ {activeSource === 'bizar' && (
66
+ <span className="chat-composer-source-hint">→ bizar chat</span>
67
+ )}
68
+ {sending && <span className="chat-composer-source-hint">sending…</span>}
73
69
  </div>
74
70
  </div>
75
71
  );
76
- }
72
+ }