@qnote/q-ai-note 1.0.2 → 1.0.4
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/dist/server/aiClient.d.ts.map +1 -1
- package/dist/server/aiClient.js +7 -0
- package/dist/server/aiClient.js.map +1 -1
- package/dist/server/api/chat.d.ts.map +1 -1
- package/dist/server/api/chat.js +3 -0
- package/dist/server/api/chat.js.map +1 -1
- package/dist/server/api/nodeEntities.d.ts +3 -0
- package/dist/server/api/nodeEntities.d.ts.map +1 -0
- package/dist/server/api/nodeEntities.js +116 -0
- package/dist/server/api/nodeEntities.js.map +1 -0
- package/dist/server/api/settings.d.ts.map +1 -1
- package/dist/server/api/settings.js +16 -1
- package/dist/server/api/settings.js.map +1 -1
- package/dist/server/config.d.ts +3 -2
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +6 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/nodeEntitiesStore.d.ts +78 -0
- package/dist/server/nodeEntitiesStore.d.ts.map +1 -0
- package/dist/server/nodeEntitiesStore.js +196 -0
- package/dist/server/nodeEntitiesStore.js.map +1 -0
- package/dist/server/react/agent.d.ts +3 -0
- package/dist/server/react/agent.d.ts.map +1 -1
- package/dist/server/react/agent.js +132 -2
- package/dist/server/react/agent.js.map +1 -1
- package/dist/server/react/prompts.d.ts +1 -0
- package/dist/server/react/prompts.d.ts.map +1 -1
- package/dist/server/react/prompts.js +25 -4
- package/dist/server/react/prompts.js.map +1 -1
- package/dist/server/react/tools.d.ts.map +1 -1
- package/dist/server/react/tools.js +105 -0
- package/dist/server/react/tools.js.map +1 -1
- package/dist/web/app.js +872 -57
- package/dist/web/index.html +78 -1
- package/dist/web/styles.css +783 -33
- package/dist/web/vueRenderers.js +228 -13
- package/package.json +1 -1
package/dist/web/vueRenderers.js
CHANGED
|
@@ -83,18 +83,46 @@ export function mountDiaryTimeline(targetId, options) {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function
|
|
86
|
+
function renderEntityBadges(summary) {
|
|
87
|
+
const issue = Number(summary?.issue || 0);
|
|
88
|
+
const knowledge = Number(summary?.knowledge || 0);
|
|
89
|
+
const capability = Number(summary?.capability || 0);
|
|
90
|
+
const issueOpen = Number(summary?.issue_open || 0);
|
|
91
|
+
const issueClosed = Number(summary?.issue_closed || 0);
|
|
92
|
+
const isBlindSpot = issue + knowledge + capability === 0;
|
|
93
|
+
const issueStateClass = issue === 0
|
|
94
|
+
? 'issue-none'
|
|
95
|
+
: issueOpen > 0
|
|
96
|
+
? 'issue-open'
|
|
97
|
+
: issueClosed > 0
|
|
98
|
+
? 'issue-closed'
|
|
99
|
+
: 'issue-none';
|
|
100
|
+
return `
|
|
101
|
+
<span
|
|
102
|
+
class="node-entity-mini-badges ${isBlindSpot ? 'blind-spot' : ''} ${issueStateClass}"
|
|
103
|
+
title="节点摘要:${issue}/${knowledge}/${capability}(Issue/Knowledge/Capability)"
|
|
104
|
+
>
|
|
105
|
+
<span class="node-entity-mini-badge ${issue > 0 || knowledge > 0 || capability > 0 ? 'active' : ''}">${issue}/${knowledge}/${capability}</span>
|
|
106
|
+
</span>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
|
|
87
111
|
const children = byParent.get(node.id) || [];
|
|
88
112
|
const hasChildren = children.length > 0;
|
|
89
113
|
const isExpanded = expandedIdSet.has(node.id);
|
|
90
114
|
const isShort = String(node.name || '').trim().length <= 10;
|
|
115
|
+
const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
|
|
91
116
|
|
|
92
117
|
if (!hasChildren) {
|
|
93
118
|
return `
|
|
94
|
-
<div class="tree-leaf-node" data-id="${esc(node.id)}" tabindex="0">
|
|
119
|
+
<div class="tree-leaf-node" data-id="${esc(node.id)}" data-select-id="${esc(node.id)}" tabindex="0">
|
|
95
120
|
<span class="node-status ${esc(node.status)}"></span>
|
|
96
|
-
<span class="node-name ${isShort ? 'short-name' : ''}">${esc(node.name)}</span>
|
|
121
|
+
<span class="node-name ${isShort ? 'short-name' : ''}" data-select-id="${esc(node.id)}">${esc(node.name)}</span>
|
|
122
|
+
${renderEntityBadges(nodeSummary)}
|
|
97
123
|
<div class="node-actions">
|
|
124
|
+
<button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
|
|
125
|
+
<button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
|
|
98
126
|
<button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
|
|
99
127
|
<button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
|
|
100
128
|
</div>
|
|
@@ -106,15 +134,24 @@ function renderTreeNode(node, byParent, expandedIdSet) {
|
|
|
106
134
|
const leafChildren = children.filter((child) => ((byParent.get(child.id) || []).length === 0));
|
|
107
135
|
|
|
108
136
|
return `
|
|
109
|
-
<div class="tree-parent-card">
|
|
110
|
-
<div class="tree-parent-header" data-
|
|
111
|
-
<
|
|
137
|
+
<div class="tree-parent-card ${isExpanded ? 'expanded' : 'collapsed'}">
|
|
138
|
+
<div class="tree-parent-header" data-select-id="${esc(node.id)}" tabindex="0" aria-expanded="${isExpanded ? 'true' : 'false'}">
|
|
139
|
+
<button
|
|
140
|
+
class="node-expand-btn ${isExpanded ? 'expanded' : ''}"
|
|
141
|
+
data-action="toggle"
|
|
142
|
+
data-id="${esc(node.id)}"
|
|
143
|
+
aria-label="${isExpanded ? '折叠节点' : '展开节点'}"
|
|
144
|
+
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
|
145
|
+
type="button"
|
|
146
|
+
>
|
|
112
147
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
|
|
113
|
-
</
|
|
148
|
+
</button>
|
|
114
149
|
<span class="node-status ${esc(node.status)}"></span>
|
|
115
150
|
<span class="node-name">${esc(node.name)}</span>
|
|
116
|
-
${
|
|
151
|
+
${renderEntityBadges(nodeSummary)}
|
|
152
|
+
${showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : ''}
|
|
117
153
|
<div class="node-actions">
|
|
154
|
+
<button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
|
|
118
155
|
<button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
|
|
119
156
|
<button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
|
|
120
157
|
<button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
|
|
@@ -122,17 +159,21 @@ function renderTreeNode(node, byParent, expandedIdSet) {
|
|
|
122
159
|
</div>
|
|
123
160
|
${isExpanded ? `
|
|
124
161
|
<div class="tree-parent-children">
|
|
125
|
-
${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet)).join('')}
|
|
162
|
+
${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)).join('')}
|
|
126
163
|
${leafChildren.length ? `
|
|
127
164
|
<div class="tree-leaf-container">
|
|
128
165
|
<div class="tree-leaf-grid">
|
|
129
166
|
${leafChildren.map((child) => {
|
|
130
167
|
const shortName = String(child.name || '').trim().length <= 10;
|
|
168
|
+
const childSummary = entitySummaryByNodeId?.[child.id] || { issue: 0, knowledge: 0, capability: 0 };
|
|
131
169
|
return `
|
|
132
|
-
<div class="tree-leaf-node" data-id="${esc(child.id)}" tabindex="0">
|
|
170
|
+
<div class="tree-leaf-node" data-id="${esc(child.id)}" data-select-id="${esc(child.id)}" tabindex="0">
|
|
133
171
|
<span class="node-status ${esc(child.status)}"></span>
|
|
134
|
-
<span class="node-name ${shortName ? 'short-name' : ''}">${esc(child.name)}</span>
|
|
172
|
+
<span class="node-name ${shortName ? 'short-name' : ''}" data-select-id="${esc(child.id)}">${esc(child.name)}</span>
|
|
173
|
+
${renderEntityBadges(childSummary)}
|
|
135
174
|
<div class="node-actions">
|
|
175
|
+
<button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(child.id)}" title="快捷提问">💬</button>
|
|
176
|
+
<button class="node-action-btn add-child" data-action="add-child" data-id="${esc(child.id)}" title="添加子任务">+</button>
|
|
136
177
|
<button class="node-action-btn" data-action="edit" data-id="${esc(child.id)}" title="编辑">✎</button>
|
|
137
178
|
<button class="node-action-btn delete" data-action="delete" data-id="${esc(child.id)}" title="删除">✕</button>
|
|
138
179
|
</div>
|
|
@@ -148,6 +189,151 @@ function renderTreeNode(node, byParent, expandedIdSet) {
|
|
|
148
189
|
`;
|
|
149
190
|
}
|
|
150
191
|
|
|
192
|
+
function applyAdaptiveLeafGridLayout(container) {
|
|
193
|
+
const MIN_WIDTH = 180;
|
|
194
|
+
const MAX_WIDTH = 320;
|
|
195
|
+
const GAP = 8;
|
|
196
|
+
const grids = container.querySelectorAll('.tree-leaf-grid, .tree-root-leaf-grid');
|
|
197
|
+
grids.forEach((grid) => {
|
|
198
|
+
const cards = grid.querySelectorAll(':scope > .tree-leaf-node');
|
|
199
|
+
const count = cards.length;
|
|
200
|
+
if (!count) return;
|
|
201
|
+
const availableWidth = Math.max(0, grid.clientWidth);
|
|
202
|
+
if (!availableWidth) return;
|
|
203
|
+
const maxColsByWidth = Math.max(1, Math.floor((availableWidth + GAP) / (MIN_WIDTH + GAP)));
|
|
204
|
+
const cols = Math.max(1, Math.min(count, maxColsByWidth));
|
|
205
|
+
const rawWidth = Math.floor((availableWidth - GAP * (cols - 1)) / cols);
|
|
206
|
+
const cardWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, rawWidth));
|
|
207
|
+
grid.style.gridTemplateColumns = `repeat(${cols}, minmax(${cardWidth}px, 1fr))`;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function applyAdaptiveRootMixedGridLayout(container) {
|
|
212
|
+
const MIN_COLUMN_WIDTH = 240;
|
|
213
|
+
const GAP = 10;
|
|
214
|
+
const grids = container.querySelectorAll('.tree-root-mixed-grid');
|
|
215
|
+
grids.forEach((grid) => {
|
|
216
|
+
const cards = grid.querySelectorAll(':scope > .root-grid-item');
|
|
217
|
+
const count = cards.length;
|
|
218
|
+
if (!count) return;
|
|
219
|
+
const availableWidth = Math.max(0, grid.clientWidth);
|
|
220
|
+
if (!availableWidth) return;
|
|
221
|
+
const cols = Math.max(1, Math.floor((availableWidth + GAP) / (MIN_COLUMN_WIDTH + GAP)));
|
|
222
|
+
grid.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
|
|
223
|
+
cards.forEach((card) => {
|
|
224
|
+
const type = card.getAttribute('data-root-item-type');
|
|
225
|
+
if (type === 'branch') {
|
|
226
|
+
const span = Math.min(2, cols);
|
|
227
|
+
card.style.gridColumn = `span ${span}`;
|
|
228
|
+
} else {
|
|
229
|
+
card.style.gridColumn = 'span 1';
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeId, depth = 0, showAssignee = false) {
|
|
236
|
+
const children = byParent.get(node.id) || [];
|
|
237
|
+
const hasChildren = children.length > 0;
|
|
238
|
+
const isExpanded = expandedIdSet.has(node.id);
|
|
239
|
+
const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
|
|
240
|
+
const useStackSummary = depth >= 3;
|
|
241
|
+
const childrenHtml = hasChildren && isExpanded
|
|
242
|
+
? `<div class="lane-tree-children">${children.map((child) => renderDenseLaneNode(child, byParent, expandedIdSet, entitySummaryByNodeId, depth + 1, showAssignee)).join('')}</div>`
|
|
243
|
+
: '';
|
|
244
|
+
return `
|
|
245
|
+
<div class="lane-tree-node" data-depth="${depth}">
|
|
246
|
+
<div class="lane-tree-node-row" data-select-id="${esc(node.id)}" data-node-id="${esc(node.id)}" tabindex="0">
|
|
247
|
+
${hasChildren ? `
|
|
248
|
+
<button
|
|
249
|
+
class="node-expand-btn dense-expand-btn ${isExpanded ? 'expanded' : ''}"
|
|
250
|
+
data-action="toggle"
|
|
251
|
+
data-id="${esc(node.id)}"
|
|
252
|
+
aria-label="${isExpanded ? '折叠节点' : '展开节点'}"
|
|
253
|
+
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
|
254
|
+
type="button"
|
|
255
|
+
>
|
|
256
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/></svg>
|
|
257
|
+
</button>
|
|
258
|
+
` : ''}
|
|
259
|
+
<span class="node-status ${esc(node.status)}"></span>
|
|
260
|
+
<div class="lane-tree-node-main ${useStackSummary ? 'stack-summary' : ''}">
|
|
261
|
+
<div class="lane-tree-title-line">
|
|
262
|
+
<span class="dense-node-name">${esc(node.name)}</span>
|
|
263
|
+
${!useStackSummary ? renderEntityBadges(nodeSummary) : ''}
|
|
264
|
+
${!useStackSummary && showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : ''}
|
|
265
|
+
</div>
|
|
266
|
+
${useStackSummary ? `
|
|
267
|
+
<div class="lane-tree-summary-line">
|
|
268
|
+
${renderEntityBadges(nodeSummary)}
|
|
269
|
+
${showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : ''}
|
|
270
|
+
</div>
|
|
271
|
+
` : ''}
|
|
272
|
+
</div>
|
|
273
|
+
<div class="node-actions">
|
|
274
|
+
<button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
|
|
275
|
+
<button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
|
|
276
|
+
<button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
|
|
277
|
+
<button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
${childrenHtml}
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function collectDenseLaneMetrics(root, byParent, depth = 0) {
|
|
286
|
+
const stack = [{ node: root, depth }];
|
|
287
|
+
let maxDepth = depth;
|
|
288
|
+
let maxNameLength = String(root?.name || '').trim().length;
|
|
289
|
+
let maxAssigneeLength = String(root?.assignee || '').trim().length;
|
|
290
|
+
let totalNodes = 0;
|
|
291
|
+
while (stack.length) {
|
|
292
|
+
const current = stack.pop();
|
|
293
|
+
if (!current?.node) continue;
|
|
294
|
+
totalNodes += 1;
|
|
295
|
+
maxDepth = Math.max(maxDepth, current.depth);
|
|
296
|
+
maxNameLength = Math.max(maxNameLength, String(current.node.name || '').trim().length);
|
|
297
|
+
maxAssigneeLength = Math.max(maxAssigneeLength, String(current.node.assignee || '').trim().length);
|
|
298
|
+
const children = byParent.get(current.node.id) || [];
|
|
299
|
+
for (const child of children) {
|
|
300
|
+
stack.push({ node: child, depth: current.depth + 1 });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { maxDepth, maxNameLength, maxAssigneeLength, totalNodes };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function getDenseLaneWidthPx(root, byParent, showAssignee = false) {
|
|
307
|
+
const MIN_WIDTH = 190;
|
|
308
|
+
const MAX_WIDTH = 420;
|
|
309
|
+
const { maxDepth, maxNameLength, maxAssigneeLength, totalNodes } = collectDenseLaneMetrics(root, byParent, 0);
|
|
310
|
+
const base = 150;
|
|
311
|
+
const nameFactor = Math.min(maxNameLength, 28) * 6;
|
|
312
|
+
const depthFactor = Math.min(maxDepth, 8) * 8;
|
|
313
|
+
const assigneeFactor = showAssignee ? Math.min(maxAssigneeLength, 18) * 5 : 0;
|
|
314
|
+
const densityFactor = Math.min(totalNodes, 20) * 2;
|
|
315
|
+
const preferred = base + nameFactor + depthFactor + assigneeFactor + densityFactor;
|
|
316
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, preferred));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
|
|
320
|
+
return `
|
|
321
|
+
<div class="dense-tree dense-horizontal dense-lane-board">
|
|
322
|
+
${roots.map((root) => {
|
|
323
|
+
const laneWidth = getDenseLaneWidthPx(root, byParent, showAssignee);
|
|
324
|
+
const laneNameMax = Math.max(120, laneWidth - 86);
|
|
325
|
+
return `
|
|
326
|
+
<section class="dense-lane" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
|
|
327
|
+
<div class="dense-lane-body">
|
|
328
|
+
${renderDenseLaneNode(root, byParent, expandedIdSet, entitySummaryByNodeId, 0, showAssignee)}
|
|
329
|
+
</div>
|
|
330
|
+
</section>
|
|
331
|
+
`;
|
|
332
|
+
}).join('')}
|
|
333
|
+
</div>
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
|
|
151
337
|
export function mountWorkTree(targetId, options) {
|
|
152
338
|
const container = byId(targetId);
|
|
153
339
|
if (!container) return;
|
|
@@ -158,6 +344,11 @@ export function mountWorkTree(targetId, options) {
|
|
|
158
344
|
onAddChild,
|
|
159
345
|
onEdit,
|
|
160
346
|
onDelete,
|
|
347
|
+
onQuickChat,
|
|
348
|
+
onSelect,
|
|
349
|
+
entitySummaryByNodeId = {},
|
|
350
|
+
renderMode = 'card',
|
|
351
|
+
showAssignee = false,
|
|
161
352
|
} = options || {};
|
|
162
353
|
|
|
163
354
|
const byParent = new Map();
|
|
@@ -173,8 +364,19 @@ export function mountWorkTree(targetId, options) {
|
|
|
173
364
|
container.innerHTML = '<div class="empty-state"><p>当前筛选条件下没有任务</p></div>';
|
|
174
365
|
return;
|
|
175
366
|
}
|
|
176
|
-
|
|
177
|
-
|
|
367
|
+
if (renderMode === 'dense') {
|
|
368
|
+
container.innerHTML = renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee);
|
|
369
|
+
} else {
|
|
370
|
+
const htmlParts = roots.map((root) => {
|
|
371
|
+
const hasChildren = ((byParent.get(root.id) || []).length > 0);
|
|
372
|
+
return `
|
|
373
|
+
<div class="root-grid-item ${hasChildren ? 'branch' : 'leaf'}" data-root-item-type="${hasChildren ? 'branch' : 'leaf'}">
|
|
374
|
+
${renderTreeNode(root, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)}
|
|
375
|
+
</div>
|
|
376
|
+
`;
|
|
377
|
+
});
|
|
378
|
+
container.innerHTML = `<div class="tree-root-mixed-grid">${htmlParts.join('')}</div>`;
|
|
379
|
+
}
|
|
178
380
|
|
|
179
381
|
container.querySelectorAll('[data-action]').forEach((el) => {
|
|
180
382
|
el.addEventListener('click', (e) => {
|
|
@@ -186,6 +388,19 @@ export function mountWorkTree(targetId, options) {
|
|
|
186
388
|
if (action === 'add-child') onAddChild?.(id);
|
|
187
389
|
if (action === 'edit') onEdit?.(id);
|
|
188
390
|
if (action === 'delete') onDelete?.(id);
|
|
391
|
+
if (action === 'quick-chat') onQuickChat?.(id, el);
|
|
189
392
|
});
|
|
190
393
|
});
|
|
394
|
+
container.querySelectorAll('[data-select-id]').forEach((el) => {
|
|
395
|
+
el.addEventListener('click', (e) => {
|
|
396
|
+
e.stopPropagation();
|
|
397
|
+
const id = el.getAttribute('data-select-id');
|
|
398
|
+
if (!id) return;
|
|
399
|
+
onSelect?.(id);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
if (renderMode !== 'dense') {
|
|
403
|
+
applyAdaptiveLeafGridLayout(container);
|
|
404
|
+
applyAdaptiveRootMixedGridLayout(container);
|
|
405
|
+
}
|
|
191
406
|
}
|