@my-life-buddies/cli 0.13.3 → 0.15.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.
- package/README.md +74 -22
- package/dist/bin/buddy.js +29 -10
- package/dist/bin/buddy.js.map +2 -2
- package/dist/bin/core.js +1480 -611
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +324 -27
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/app.css +65 -6
- package/dist/web/app.js +103 -39
- package/dist/web/diagnostics.js +184 -0
- package/dist/web/index.html +34 -34
- package/dist/web/widgets.css +59 -28
- package/dist/web/widgets.js +230 -76
- package/package.json +3 -3
- package/resources/README.md +6 -50
- package/resources/agent-template/package-lock.json +4 -4
- package/resources/agent-template/package.json +2 -2
- package/resources/buddy-creator/INSTALL.md +0 -25
- package/resources/buddy-creator/PATCHES.md +0 -53
- package/resources/buddy-creator/SKILL.md +0 -76
- package/resources/buddy-creator/THIRD_PARTY_NOTICES.md +0 -154
- package/resources/buddy-creator/agents/openai.yaml +0 -4
- package/resources/buddy-creator/assets/licenses/bail.txt +0 -26
- package/resources/buddy-creator/assets/licenses/ccount.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities-html4.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities-legacy.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/character-reference-invalid.txt +0 -26
- package/resources/buddy-creator/assets/licenses/comma-separated-tokens.txt +0 -26
- package/resources/buddy-creator/assets/licenses/debug.txt +0 -24
- package/resources/buddy-creator/assets/licenses/decode-named-character-reference.txt +0 -26
- package/resources/buddy-creator/assets/licenses/dequal.txt +0 -25
- package/resources/buddy-creator/assets/licenses/devlop.txt +0 -26
- package/resources/buddy-creator/assets/licenses/escape-string-regexp.txt +0 -13
- package/resources/buddy-creator/assets/licenses/estree-util-is-identifier-name.txt +0 -26
- package/resources/buddy-creator/assets/licenses/extend.txt +0 -27
- package/resources/buddy-creator/assets/licenses/hast-util-to-jsx-runtime.txt +0 -26
- package/resources/buddy-creator/assets/licenses/hast-util-whitespace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/html-url-attributes.txt +0 -25
- package/resources/buddy-creator/assets/licenses/inline-style-parser.txt +0 -13
- package/resources/buddy-creator/assets/licenses/is-alphabetical.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-alphanumerical.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-decimal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-hexadecimal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/is-plain-obj.txt +0 -13
- package/resources/buddy-creator/assets/licenses/longest-streak.txt +0 -26
- package/resources/buddy-creator/assets/licenses/markdown-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-find-and-replace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-from-markdown.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-autolink-literal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-footnote.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-strikethrough.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm-task-list-item.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-expression.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdx-jsx.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-mdxjs-esm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-phrasing.txt +0 -27
- package/resources/buddy-creator/assets/licenses/mdast-util-to-hast.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-to-markdown.txt +0 -26
- package/resources/buddy-creator/assets/licenses/mdast-util-to-string.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-core-commonmark.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-autolink-literal.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-footnote.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-strikethrough.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-table.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-tagfilter.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm-task-list-item.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-extension-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-destination.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-label.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-space.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-title.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-factory-whitespace.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-character.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-chunked.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-classify-character.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-combine-extensions.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-numeric-character-reference.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-decode-string.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-encode.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-html-tag-name.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-normalize-identifier.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-resolve-all.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-sanitize-uri.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-subtokenize.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-symbol.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark-util-types.txt +0 -26
- package/resources/buddy-creator/assets/licenses/micromark.txt +0 -26
- package/resources/buddy-creator/assets/licenses/ms.txt +0 -25
- package/resources/buddy-creator/assets/licenses/parse-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/phosphor-icons--react.txt +0 -25
- package/resources/buddy-creator/assets/licenses/property-information.txt +0 -26
- package/resources/buddy-creator/assets/licenses/react-dom.txt +0 -25
- package/resources/buddy-creator/assets/licenses/react-markdown.txt +0 -25
- package/resources/buddy-creator/assets/licenses/react.txt +0 -25
- package/resources/buddy-creator/assets/licenses/remark-gfm.txt +0 -26
- package/resources/buddy-creator/assets/licenses/remark-parse.txt +0 -25
- package/resources/buddy-creator/assets/licenses/remark-rehype.txt +0 -26
- package/resources/buddy-creator/assets/licenses/remark-stringify.txt +0 -25
- package/resources/buddy-creator/assets/licenses/scheduler.txt +0 -25
- package/resources/buddy-creator/assets/licenses/space-separated-tokens.txt +0 -26
- package/resources/buddy-creator/assets/licenses/stringify-entities.txt +0 -26
- package/resources/buddy-creator/assets/licenses/style-to-js.txt +0 -26
- package/resources/buddy-creator/assets/licenses/style-to-object.txt +0 -26
- package/resources/buddy-creator/assets/licenses/trim-lines.txt +0 -26
- package/resources/buddy-creator/assets/licenses/trough.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--debug.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--estree-jsx.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--estree.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--hast.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--mdast.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--ms.txt +0 -25
- package/resources/buddy-creator/assets/licenses/types--unist.txt +0 -25
- package/resources/buddy-creator/assets/licenses/ungap--structured-clone.txt +0 -19
- package/resources/buddy-creator/assets/licenses/unified.txt +0 -25
- package/resources/buddy-creator/assets/licenses/unist-util-is.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-position.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-stringify-position.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-visit-parents.txt +0 -26
- package/resources/buddy-creator/assets/licenses/unist-util-visit.txt +0 -26
- package/resources/buddy-creator/assets/licenses/vfile-message.txt +0 -26
- package/resources/buddy-creator/assets/licenses/vfile.txt +0 -25
- package/resources/buddy-creator/assets/licenses/zwitch.txt +0 -26
- package/resources/buddy-creator/assets/preview/assets/index-B5VmkoSx.js +0 -39
- package/resources/buddy-creator/assets/preview/assets/index-DWKP6b-3.css +0 -4
- package/resources/buddy-creator/assets/preview/brand-logo.png +0 -0
- package/resources/buddy-creator/assets/preview/brand.css +0 -100
- package/resources/buddy-creator/assets/preview/index.html +0 -7
- package/resources/buddy-creator/assets/preview/reader.css +0 -165
- package/resources/buddy-creator/assets/preview/reader.js +0 -135
- package/resources/buddy-creator/assets/preview/widgets.css +0 -1
- package/resources/buddy-creator/assets/preview/widgets.js +0 -168
- package/resources/buddy-creator/assets/widget-reference/conversation.png +0 -0
- package/resources/buddy-creator/assets/widget-reference/expanded.png +0 -0
- package/resources/buddy-creator/assets/widget-v2/app.js +0 -62
- package/resources/buddy-creator/assets/widget-v2/style.css +0 -1
- package/resources/buddy-creator/assets/widget-v3/app.js +0 -65
- package/resources/buddy-creator/assets/widget-v3/style.css +0 -103
- package/resources/buddy-creator/assets/widget-v4/app.js +0 -66
- package/resources/buddy-creator/assets/widget-v4/style.css +0 -187
- package/resources/buddy-creator/references/artifact-schema.md +0 -70
- package/resources/buddy-creator/references/catalog.json +0 -401
- package/resources/buddy-creator/references/definition.md +0 -24
- package/resources/buddy-creator/references/dialogue-examples.md +0 -133
- package/resources/buddy-creator/references/host-guide.md +0 -219
- package/resources/buddy-creator/references/interview.md +0 -235
- package/resources/buddy-creator/references/knowledge.md +0 -40
- package/resources/buddy-creator/references/methods.md +0 -77
- package/resources/buddy-creator/references/opening.md +0 -14
- package/resources/buddy-creator/references/preview-panel.md +0 -66
- package/resources/buddy-creator/references/recovery.md +0 -46
- package/resources/buddy-creator/references/service.md +0 -76
- package/resources/buddy-creator/references/widgets-protocol.md +0 -83
- package/resources/buddy-creator/references/widgets.md +0 -99
- package/resources/buddy-creator/scripts/buddy.py +0 -108
- package/resources/buddy-creator/scripts/buddy_client.py +0 -67
- package/resources/buddy-creator/scripts/buddy_core.py +0 -865
- package/resources/buddy-creator/scripts/completion.py +0 -296
- package/resources/buddy-creator/scripts/interview.py +0 -215
- package/resources/buddy-creator/scripts/preview.py +0 -617
- package/resources/buddy-creator/scripts/preview_panel.py +0 -80
- package/resources/buddy-creator/scripts/sources.py +0 -239
- package/resources/buddy-creator/scripts/widget_render.py +0 -66
- package/resources/buddy-creator/scripts/widget_render_v1.py +0 -176
- package/resources/buddy-creator/scripts/widget_render_v2.py +0 -98
- package/resources/buddy-creator/scripts/widget_render_v3.py +0 -86
- package/resources/buddy-creator/scripts/widget_render_v4.py +0 -121
- package/resources/buddy-creator/scripts/widgets.py +0 -227
- package/resources/buddy-creator/version.json +0 -8
- package/resources/buddy-creator.manifest.json +0 -791
- package/resources/dada-object-avatar/SKILL.md +0 -37
- package/resources/dada-object-avatar/agents/openai.yaml +0 -4
- package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
- package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
- package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
- package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
- package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
- package/resources/dada-object-avatar/assets/examples/work.png +0 -0
- package/resources/dada-object-avatar/assets/preview.png +0 -0
- package/resources/dada-object-avatar/references/evaluation-rubric.md +0 -22
- package/resources/dada-object-avatar/references/examples.json +0 -8
- package/resources/dada-object-avatar/references/prompt-template.md +0 -27
- package/resources/dada-object-avatar/references/style-system.md +0 -54
- package/resources/dada-object-avatar/references/subject-design.md +0 -23
- package/resources/dada-object-avatar/references/training-guide.md +0 -14
- package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
- package/resources/dada-object-avatar/references/training-record.schema.json +0 -20
- package/resources/dada-object-avatar/scripts/choose_gaze.py +0 -36
- package/resources/dada-object-avatar/scripts/record_example.py +0 -22
- package/resources/dada-object-avatar/scripts/validate_png.py +0 -52
- package/resources/dada-object-avatar.manifest.json +0 -85
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""Composed read-only visual templates, sharing their HTML composition with SVG exports."""
|
|
2
|
-
import html
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import buddy_core as core
|
|
6
|
-
|
|
7
|
-
ASSETS = Path(__file__).resolve().parents[1] / 'assets/widget-v2'
|
|
8
|
-
ICONS = {
|
|
9
|
-
'route': '<circle cx="6" cy="6" r="3"/><circle cx="18" cy="18" r="3"/><path d="M9 6h7a4 4 0 0 1 0 8H8a4 4 0 0 0 0 8"/>',
|
|
10
|
-
'ticket': '<path d="M3 5h18v5a2 2 0 0 0 0 4v5H3v-5a2 2 0 0 0 0-4Z"/><path d="M15 5v3m0 3v2m0 3v3"/>',
|
|
11
|
-
'wallet': '<path d="M20 8V5H6a3 3 0 0 0 0 6h15v9H5a3 3 0 0 1-3-3V8"/><path d="M21 12h-6v5h6"/><circle cx="17" cy="14.5" r=".5"/>',
|
|
12
|
-
'bag': '<rect x="4" y="7" width="16" height="14" rx="4"/><path d="M8 7V5a4 4 0 0 1 8 0v2M8 13h8m-8 4h5"/>',
|
|
13
|
-
'activity': '<path d="M3 12h4l3-7 4 14 3-7h4"/>',
|
|
14
|
-
'document': '<path d="M5 3h10l4 4v14H5Z"/><path d="M14 3v5h5M8 12h8m-8 4h5"/>',
|
|
15
|
-
'bed': '<path d="M3 19V8m18 11V8M3 15h18M5 15V6h14v9M7 10h4v5m2-5h4v5"/>',
|
|
16
|
-
'train': '<rect x="5" y="2" width="14" height="17" rx="5"/><path d="M5 10h14M8 19l-2 3m10-3 2 3M12 3v7"/><circle cx="9" cy="15" r="1"/><circle cx="15" cy="15" r="1"/>',
|
|
17
|
-
'coffee': '<path d="M3 9h13v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Zm13 1h2a3 3 0 0 1 0 6h-2M7 3v3m5-3v3"/>',
|
|
18
|
-
'pin': '<path d="M19 10c0 5-7 11-7 11S5 15 5 10a7 7 0 0 1 14 0Z"/><circle cx="12" cy="10" r="2"/>',
|
|
19
|
-
'check': '<path d="m5 12 4 4L19 6"/>',
|
|
20
|
-
'clock': '<circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/>',
|
|
21
|
-
'info': '<circle cx="12" cy="12" r="9"/><path d="M12 11v6m0-11v1"/>',
|
|
22
|
-
'arrow': '<path d="M4 12h15m-6-6 6 6-6 6"/>',
|
|
23
|
-
'chevron': '<path d="m9 5 7 7-7 7"/>',
|
|
24
|
-
'close': '<path d="m6 6 12 12M6 18 18 6"/>',
|
|
25
|
-
'signal': '<path d="M4 19v-4m5 4v-8m5 8V7m5 12V3"/>',
|
|
26
|
-
'battery': '<rect x="2" y="6" width="17" height="12" rx="3"/><path d="M22 10v4M5 9h11v6H5Z"/>',
|
|
27
|
-
'plus': '<path d="M12 4v16M4 12h16"/>',
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
def esc(value):
|
|
31
|
-
return html.escape(str(value), quote=True)
|
|
32
|
-
|
|
33
|
-
def icon(name):
|
|
34
|
-
return '<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" aria-hidden="true">' + ICONS.get(name, ICONS['document']) + '</svg>'
|
|
35
|
-
|
|
36
|
-
def metrics(values, class_name):
|
|
37
|
-
cells = ''.join('<div class="metric"><span class="metric-label">'+esc(m['label'])+'</span><span class="metric-value">'+esc(m['value'])+'</span></div>' for m in values)
|
|
38
|
-
return '<div class="'+class_name+'" style="--metric-count:'+str(max(1,len(values)))+'">'+cells+'</div>' if values else ''
|
|
39
|
-
|
|
40
|
-
def overview(sample):
|
|
41
|
-
value = sample['overview']
|
|
42
|
-
result = '<div class="overview"><p class="eyebrow">'+esc(value['eyebrow'])+'</p><h3 class="overview-title">'+esc(value['title'])+'</h3><p class="overview-subtitle">'+esc(value['subtitle'])+'</p>'
|
|
43
|
-
result += metrics(value['metrics'], 'overview-metrics')
|
|
44
|
-
if value.get('progress'):
|
|
45
|
-
p=value['progress']; percentage=p['value']/p['max']*100
|
|
46
|
-
result += '<div class="progress-track" role="img" aria-label="'+esc(p['label'])+' '+str(round(percentage,1))+'%"><div class="progress-fill" style="width:'+str(percentage)+'%"></div></div><p class="progress-caption">'+esc(p['label'])+'</p>'
|
|
47
|
-
return result+'</div>'
|
|
48
|
-
|
|
49
|
-
def row_html(row, default_icon):
|
|
50
|
-
status=row.get('status'); tone=status['tone'] if status else ''
|
|
51
|
-
result='<div class="row '+tone+'"><span class="row-symbol">'+icon(row.get('icon',default_icon))+'</span><div class="row-content"><div class="row-label">'+esc(row['label'])+'</div><p class="row-value">'+esc(row['value'])+'</p>'
|
|
52
|
-
if row.get('caption'): result+='<p class="row-caption">'+esc(row['caption'])+'</p>'
|
|
53
|
-
if status: result+='<span class="status '+tone+'">'+esc(status['label'])+'</span>'
|
|
54
|
-
result+='</div>'
|
|
55
|
-
if row.get('detail'):
|
|
56
|
-
result+='<button class="detail-open" aria-label="查看说明:'+esc(row['label'])+'">'+icon('chevron')+'</button><span class="detail-copy" hidden="hidden">'+esc(row['detail'])+'</span>'
|
|
57
|
-
return result+'</div>'
|
|
58
|
-
|
|
59
|
-
def screen(data,item,sample,expanded=False):
|
|
60
|
-
design=data['design']; interactions=item['readInteractions']; ov=sample['overview']
|
|
61
|
-
selected=sample['sections'][0]['id'] if 'tabs' in interactions else 'all'
|
|
62
|
-
result='<div class="screen '+design['layout']+'" data-state="'+sample['id']+'" style="--accent:'+design['accent']+';--title-size:'+str(design['titleSize'])+'px">'
|
|
63
|
-
result+='<div class="statusbar"><span>9:41</span><span class="device">'+icon('signal')+icon('battery')+'</span></div>'
|
|
64
|
-
result+='<header class="chat-header"><span class="back">‹</span><span class="header-mark">'+esc(design['conversationTitle'][:1])+'</span><div class="buddy-identity"><strong>'+esc(design['conversationTitle'])+'</strong><small>随对话整理,随时回来查看</small></div><span>···</span></header>'
|
|
65
|
-
result+='<main class="chat-body"><p class="chat-date">对话示例</p><div class="message user"><div class="bubble">'+esc(sample['userMessage'])+'</div></div><div class="message assistant"><span class="avatar">'+esc(design['conversationTitle'][:1])+'</span><div class="bubble">'+esc(sample['buddyMessage'])+'</div></div>'
|
|
66
|
-
result+='<article class="chat-card"><div class="card-top"><span class="identity-icon">'+icon(design['icon'])+'</span><div><p class="card-kind">'+esc(ov['eyebrow'])+'</p><h1>'+esc(item['name'])+'</h1></div></div><p class="card-summary">'+esc(sample['summary'])+'</p>'
|
|
67
|
-
result+=metrics(ov['metrics'][:2],'card-meta')+'<button class="open-sheet"><span>去查看</span>'+icon('arrow')+'</button><div class="card-foot">'+icon('clock')+'<span>内容随对话更新</span></div></article></main>'
|
|
68
|
-
result+='<div class="composer">'+icon('plus')+'<span>修改请在对话中告诉搭子</span>'+icon('arrow')+'</div><div class="home-indicator"></div>'
|
|
69
|
-
result+='<div class="overlay"'+('' if expanded else ' hidden="hidden"')+'><section class="sheet" role="dialog" aria-modal="true" aria-label="小挂件展开页"><div class="sheet-header"><span class="identity-icon">'+icon(design['icon'])+'</span><h2>'+esc(item['name'])+'</h2><button class="close sheet-close" aria-label="关闭小挂件">'+icon('close')+'</button></div><div class="sheet-body">'
|
|
70
|
-
result+=overview(sample)
|
|
71
|
-
if 'tabs' in interactions or 'filter' in interactions:
|
|
72
|
-
options=sample['sections'] if 'tabs' in interactions else [{'id':'all','title':'全部'}]+sample['sections']
|
|
73
|
-
result+='<nav class="reading-nav" aria-label="筛选已有内容">'
|
|
74
|
-
for group in options:
|
|
75
|
-
result+='<button data-group="'+esc(group['id'])+'" aria-pressed="'+str(group['id']==selected).lower()+'">'+esc(group['title'])+'</button>'
|
|
76
|
-
result+='</nav>'
|
|
77
|
-
for group in sample['sections']:
|
|
78
|
-
collapsed='collapse' in interactions;tag='details' if collapsed else 'section';heading='summary' if collapsed else 'h3'
|
|
79
|
-
result+='<'+tag+' class="section" data-section="'+esc(group['id'])+'"'+(' open="open"' if collapsed else '')+(' hidden="hidden"' if selected!='all' and selected!=group['id'] else '')+'>'
|
|
80
|
-
result+='<'+heading+' class="section-heading">'+esc(group['title'])+'<span class="section-count">'+str(len(group['rows']))+'</span>'+(icon('chevron') if collapsed else '')+'</'+heading+'>'
|
|
81
|
-
result+=''.join(row_html(row,design['icon']) for row in group['rows'])+'</'+tag+'>'
|
|
82
|
-
result+='<p class="note">'+icon('info')+'<span>如需修改内容,请回到对话中告诉搭子。</span></p></div></section><div class="detail-layer" hidden="hidden"><section class="detail-box" role="dialog" aria-modal="true" aria-label="只读详情"><div class="detail-head"><h3 class="detail-title">详情</h3><button class="close detail-close" aria-label="关闭详情">'+icon('close')+'</button></div><p class="detail-text"></p><p class="detail-foot">只读详情 · 修改通过对话完成</p></section></div></div></div>'
|
|
83
|
-
return result
|
|
84
|
-
|
|
85
|
-
def files(artifact,item=None):
|
|
86
|
-
item=item or artifact.get('renderItem')
|
|
87
|
-
core.require(item,'WIDGET_RENDER_ITEM','生成原型需要对应清单项。')
|
|
88
|
-
data=artifact['data'];css=(ASSETS/'style.css').read_bytes();js=(ASSETS/'app.js').read_bytes()
|
|
89
|
-
screens=''.join(screen(data,item,sample) for sample in data['states'])
|
|
90
|
-
page='<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>'+esc(item['name'])+' · 小挂件原型</title><link rel="stylesheet" href="style.css"/></head><body>'+screens+'<script src="app.js"></script></body></html>'
|
|
91
|
-
result={'index.html':page.encode('utf-8'),'style.css':css,'app.js':js}
|
|
92
|
-
d=data['design'];w,h=d['screenWidth'],d['screenHeight']
|
|
93
|
-
for sample in data['states']:
|
|
94
|
-
for view in ('chat','expanded'):
|
|
95
|
-
content=screen(data,item,sample,view=='expanded')
|
|
96
|
-
svg='<svg xmlns="http://www.w3.org/2000/svg" width="'+str(w)+'" height="'+str(h)+'" viewBox="0 0 '+str(w)+' '+str(h)+'"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" class="svg-export" style="width:'+str(w)+'px;height:'+str(h)+'px"><style>'+html.escape(css.decode())+'</style>'+content+'</div></foreignObject></svg>'
|
|
97
|
-
result[view+'-'+sample['id']+'.svg']=svg.encode('utf-8')
|
|
98
|
-
return result
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"""Quiet, content-led prototypes. Earlier renderers remain immutable."""
|
|
2
|
-
import html
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import buddy_core as core
|
|
6
|
-
from widget_render_v2 import esc, icon, metrics
|
|
7
|
-
|
|
8
|
-
ASSETS = Path(__file__).resolve().parents[1] / 'assets/widget-v3'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def overview(sample, layout):
|
|
12
|
-
value = sample['overview']
|
|
13
|
-
result = '<div class="overview"><h3>'+esc(value['title'])+'</h3><p class="overview-subtitle">'+esc(value['subtitle'])+'</p>'
|
|
14
|
-
if layout == 'ledger':
|
|
15
|
-
result += metrics(value['metrics'], 'overview-metrics')
|
|
16
|
-
elif layout == 'checklist':
|
|
17
|
-
result += '<p class="reading-meta">' + esc(' · '.join(m['label']+' '+m['value'] for m in value['metrics'])) + '</p>'
|
|
18
|
-
if layout in ('ledger', 'checklist') and value.get('progress'):
|
|
19
|
-
progress = value['progress']
|
|
20
|
-
percentage = progress['value'] / progress['max'] * 100
|
|
21
|
-
result += '<div class="progress-track" role="img" aria-label="'+esc(progress['label'])+' '+str(round(percentage, 1))+'%"><div style="width:'+str(percentage)+'%"></div></div>'
|
|
22
|
-
if layout == 'ledger':
|
|
23
|
-
result += '<p class="progress-caption">'+esc(progress['label'])+'</p>'
|
|
24
|
-
return result+'</div>'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def row_html(row, layout):
|
|
28
|
-
status = row.get('status')
|
|
29
|
-
result = '<div class="row">'
|
|
30
|
-
if layout == 'timeline':
|
|
31
|
-
result += '<div class="time">'+esc(row['label'])+'</div>'
|
|
32
|
-
result += '<div class="row-content">'
|
|
33
|
-
if layout != 'timeline':
|
|
34
|
-
result += '<div class="row-label">'+esc(row['label'])+'</div>'
|
|
35
|
-
result += '<p class="row-value">'+esc(row['value'])+'</p>'
|
|
36
|
-
if row.get('caption'):
|
|
37
|
-
result += '<p class="row-caption">'+esc(row['caption'])+'</p>'
|
|
38
|
-
# A state already written in the sentence does not need a duplicate badge.
|
|
39
|
-
if status and status['label'] not in row['value'] and status['label'] not in row.get('caption', ''):
|
|
40
|
-
result += '<span class="status '+status['tone']+'">'+esc(status['label'])+'</span>'
|
|
41
|
-
result += '</div>'
|
|
42
|
-
if row.get('detail'):
|
|
43
|
-
result += '<button class="detail-open" aria-label="查看说明:'+esc(row['label'])+'">'+icon('chevron')+'</button><span class="detail-copy" hidden="hidden">'+esc(row['detail'])+'</span>'
|
|
44
|
-
return result+'</div>'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def screen(data, item, sample, expanded=False):
|
|
48
|
-
design = data['design']; layout = design['layout']; interactions = item['readInteractions']
|
|
49
|
-
selected = sample['sections'][0]['id'] if 'tabs' in interactions else 'all'
|
|
50
|
-
name = design['conversationTitle']
|
|
51
|
-
result = '<div class="screen '+layout+'" data-state="'+sample['id']+'" style="--accent:'+design['accent']+';--title-size:'+str(design['titleSize'])+'px">'
|
|
52
|
-
result += '<div class="statusbar"><span>9:41</span><span class="device">'+icon('signal')+icon('battery')+'</span></div><header class="chat-header"><span class="back">‹</span><strong>'+esc(name)+'</strong><span>···</span></header>'
|
|
53
|
-
result += '<main class="chat-body"><p class="chat-date">对话示例</p><div class="message user"><div class="bubble">'+esc(sample['userMessage'])+'</div></div><div class="message assistant"><span class="avatar">'+esc(name[:1])+'</span><div class="bubble">'+esc(sample['buddyMessage'])+'</div></div>'
|
|
54
|
-
result += '<article class="chat-card"><div class="card-content"><p class="card-kind">'+esc(item['name'])+'</p><h1>'+esc(sample['overview']['title'])+'</h1><p class="card-summary">'+esc(sample['summary'])+'</p></div><button class="open-sheet"><span>去查看</span>'+icon('arrow')+'</button></article></main>'
|
|
55
|
-
result += '<div class="composer">'+icon('plus')+'<span>修改请在对话中告诉搭子</span>'+icon('arrow')+'</div><div class="home-indicator"></div>'
|
|
56
|
-
result += '<div class="overlay"'+('' if expanded else ' hidden="hidden"')+'><section class="sheet" tabindex="-1" role="dialog" aria-modal="true" aria-label="小挂件展开页"><div class="sheet-header"><h2>'+esc(item['name'])+'</h2><button class="close sheet-close" aria-label="关闭小挂件">'+icon('close')+'</button></div><div class="sheet-body">'+overview(sample, layout)
|
|
57
|
-
if 'tabs' in interactions or 'filter' in interactions:
|
|
58
|
-
options = sample['sections'] if 'tabs' in interactions else [{'id':'all','title':'全部'}]+sample['sections']
|
|
59
|
-
result += '<nav class="reading-nav" aria-label="筛选已有内容">'
|
|
60
|
-
for group in options:
|
|
61
|
-
result += '<button data-group="'+esc(group['id'])+'" aria-pressed="'+str(group['id']==selected).lower()+'">'+esc(group['title'])+'</button>'
|
|
62
|
-
result += '</nav>'
|
|
63
|
-
for group in sample['sections']:
|
|
64
|
-
collapsible = 'collapse' in interactions
|
|
65
|
-
tag = 'details' if collapsible else 'section'; heading = 'summary' if collapsible else 'h3'
|
|
66
|
-
result += '<'+tag+' class="section" data-section="'+esc(group['id'])+'"'+(' open="open"' if collapsible else '')+(' hidden="hidden"' if selected!='all' and selected!=group['id'] else '')+'>'
|
|
67
|
-
result += '<'+heading+' class="section-heading">'+esc(group['title'])+(icon('chevron') if collapsible else '')+'</'+heading+'>'
|
|
68
|
-
result += ''.join(row_html(row, layout) for row in group['rows'])+'</'+tag+'>'
|
|
69
|
-
result += '<p class="note">如需修改,请在对话中告诉搭子。</p></div></section><div class="detail-layer" hidden="hidden"><section class="detail-box" role="dialog" aria-modal="true" aria-label="只读详情"><div class="detail-head"><h3 class="detail-title">详情</h3><button class="close detail-close" aria-label="关闭详情">'+icon('close')+'</button></div><p class="detail-text"></p><p class="detail-foot">修改请通过对话完成</p></section></div></div></div>'
|
|
70
|
-
return result
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def files(artifact, item=None):
|
|
74
|
-
item = item or artifact.get('renderItem')
|
|
75
|
-
core.require(item, 'WIDGET_RENDER_ITEM', '生成原型需要对应清单项。')
|
|
76
|
-
data = artifact['data']; css = (ASSETS/'style.css').read_bytes(); js = (ASSETS/'app.js').read_bytes()
|
|
77
|
-
screens = ''.join(screen(data, item, sample) for sample in data['states'])
|
|
78
|
-
page = '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>'+esc(item['name'])+' · 小挂件原型</title><link rel="stylesheet" href="style.css"/></head><body>'+screens+'<script src="app.js"></script></body></html>'
|
|
79
|
-
result = {'index.html':page.encode('utf-8'), 'style.css':css, 'app.js':js}
|
|
80
|
-
w, h = data['design']['screenWidth'], data['design']['screenHeight']
|
|
81
|
-
for sample in data['states']:
|
|
82
|
-
for view in ('chat', 'expanded'):
|
|
83
|
-
content = screen(data, item, sample, view=='expanded')
|
|
84
|
-
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="'+str(w)+'" height="'+str(h)+'" viewBox="0 0 '+str(w)+' '+str(h)+'"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" class="svg-export" style="width:'+str(w)+'px;height:'+str(h)+'px"><style>'+html.escape(css.decode())+'</style>'+content+'</div></foreignObject></svg>'
|
|
85
|
-
result[view+'-'+sample['id']+'.svg'] = svg.encode('utf-8')
|
|
86
|
-
return result
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Task-specific layouts and chat previews within one visual family."""
|
|
2
|
-
import html
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import buddy_core as core
|
|
6
|
-
from widget_render_v2 import esc, icon, metrics
|
|
7
|
-
|
|
8
|
-
ASSETS = Path(__file__).resolve().parents[1] / 'assets/widget-v4'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def change_pair(row):
|
|
12
|
-
change = row['change']
|
|
13
|
-
proposed = row['status']['tone'] == 'pending'
|
|
14
|
-
before_label, after_label = ('原安排', '建议调整') if proposed else ('调整前', '调整后')
|
|
15
|
-
return '<div class="change-pair"><div class="before"><small>'+before_label+'</small><p>'+esc(change['before'])+'</p></div><span class="change-arrow" aria-hidden="true">→</span><div class="after"><small>'+after_label+'</small><p>'+esc(change['after'])+'</p></div></div>'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def card_preview(data, sample):
|
|
19
|
-
layout = data['design']['layout']
|
|
20
|
-
groups = sample['sections']
|
|
21
|
-
if layout == 'timeline':
|
|
22
|
-
return '<div class="calendar-strip">'+''.join('<div><span>'+esc(group['title'])+'</span><small>'+esc(group['rows'][0]['label'])+'</small></div>' for group in groups[:4])+'</div>'
|
|
23
|
-
if layout == 'cards':
|
|
24
|
-
return '<div class="booking-reference"><span>'+esc(groups[0]['title'])+'</span><p>'+esc(groups[0]['rows'][0]['label'])+'</p></div>'
|
|
25
|
-
if layout == 'ledger':
|
|
26
|
-
return metrics(sample['overview']['metrics'][:2], 'receipt-totals')
|
|
27
|
-
if layout == 'checklist':
|
|
28
|
-
return '<div class="packing-index">'+''.join('<div><small>'+esc(group['title'])+'</small><p>'+esc(group['rows'][0]['label'])+'</p></div>' for group in groups[:4])+'</div>'
|
|
29
|
-
if layout == 'changes':
|
|
30
|
-
row = next((row for group in groups for row in group['rows'] if row.get('change')), None)
|
|
31
|
-
if row:
|
|
32
|
-
return '<div class="change-preview"><div class="change-preview-label">'+esc(row['value'])+' · '+esc(row['status']['label'])+'</div>'+change_pair(row)+'</div>'
|
|
33
|
-
return ''
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def chat_card(data, item, sample):
|
|
37
|
-
return '<article class="chat-card"><div class="card-content"><p class="card-kind">'+esc(item['name'])+'</p><h1>'+esc(sample['overview']['title'])+'</h1>'+card_preview(data, sample)+'<p class="card-summary">'+esc(sample['summary'])+'</p></div><button class="open-sheet"><span>去查看</span>'+icon('arrow')+'</button></article>'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def overview(sample, layout):
|
|
41
|
-
value = sample['overview']
|
|
42
|
-
result = '<div class="overview"><h3>'+esc(value['title'])+'</h3><p class="overview-subtitle">'+esc(value['subtitle'])+'</p>'
|
|
43
|
-
if layout == 'ledger':
|
|
44
|
-
result += metrics(value['metrics'], 'overview-metrics')
|
|
45
|
-
elif layout == 'checklist':
|
|
46
|
-
result += '<p class="reading-meta">' + esc(' · '.join(m['label']+' '+m['value'] for m in value['metrics'])) + '</p>'
|
|
47
|
-
if layout in ('ledger', 'checklist') and value.get('progress'):
|
|
48
|
-
progress = value['progress']
|
|
49
|
-
percentage = progress['value'] / progress['max'] * 100
|
|
50
|
-
result += '<div class="progress-track" role="img" aria-label="'+esc(progress['label'])+' '+str(round(percentage, 1))+'%"><div style="width:'+str(percentage)+'%"></div></div>'
|
|
51
|
-
if layout == 'ledger':
|
|
52
|
-
result += '<p class="progress-caption">'+esc(progress['label'])+'</p>'
|
|
53
|
-
return result+'</div>'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def row_html(row, layout):
|
|
57
|
-
status = row.get('status')
|
|
58
|
-
result = '<div class="row">'
|
|
59
|
-
if layout == 'timeline':
|
|
60
|
-
result += '<div class="time">'+esc(row['label'])+'</div>'
|
|
61
|
-
result += '<div class="row-content">'
|
|
62
|
-
if layout == 'changes':
|
|
63
|
-
result += '<div class="change-heading"><div class="row-label">'+esc(row['value'])+'</div><span class="change-time">'+esc(row['label'])+'</span></div>'
|
|
64
|
-
elif layout != 'timeline':
|
|
65
|
-
result += '<div class="row-label">'+esc(row['label'])+'</div>'
|
|
66
|
-
if layout != 'changes':
|
|
67
|
-
result += '<p class="row-value">'+esc(row['value'])+'</p>'
|
|
68
|
-
if row.get('change'):
|
|
69
|
-
result += change_pair(row)
|
|
70
|
-
if row.get('caption'):
|
|
71
|
-
result += '<p class="row-caption">'+esc(row['caption'])+'</p>'
|
|
72
|
-
# A state already written in the sentence does not need a duplicate badge.
|
|
73
|
-
if status and status['label'] not in row['value'] and status['label'] not in row.get('caption', ''):
|
|
74
|
-
result += '<span class="status '+status['tone']+'">'+esc(status['label'])+'</span>'
|
|
75
|
-
result += '</div>'
|
|
76
|
-
if row.get('detail'):
|
|
77
|
-
result += '<button class="detail-open" aria-label="查看说明:'+esc(row['label'])+'">'+icon('chevron')+'</button><span class="detail-copy" hidden="hidden">'+esc(row['detail'])+'</span>'
|
|
78
|
-
return result+'</div>'
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def screen(data, item, sample, expanded=False):
|
|
82
|
-
design = data['design']; layout = design['layout']; interactions = item['readInteractions']
|
|
83
|
-
selected = sample['sections'][0]['id'] if 'tabs' in interactions else 'all'
|
|
84
|
-
name = design['conversationTitle']
|
|
85
|
-
result = '<div class="screen '+layout+'" data-state="'+sample['id']+'" style="--accent:'+design['accent']+';--title-size:'+str(design['titleSize'])+'px">'
|
|
86
|
-
result += '<div class="statusbar"><span>9:41</span><span class="device">'+icon('signal')+icon('battery')+'</span></div><header class="chat-header"><span class="back">‹</span><strong>'+esc(name)+'</strong><span>···</span></header>'
|
|
87
|
-
result += '<main class="chat-body"><p class="chat-date">对话示例</p><div class="message user"><div class="bubble">'+esc(sample['userMessage'])+'</div></div><div class="message assistant"><span class="avatar">'+esc(name[:1])+'</span><div class="bubble">'+esc(sample['buddyMessage'])+'</div></div>'
|
|
88
|
-
result += chat_card(data, item, sample)+'</main>'
|
|
89
|
-
result += '<div class="composer">'+icon('plus')+'<span>修改请在对话中告诉搭子</span>'+icon('arrow')+'</div><div class="home-indicator"></div>'
|
|
90
|
-
result += '<div class="overlay"'+('' if expanded else ' hidden="hidden"')+'><section class="sheet" tabindex="-1" role="dialog" aria-modal="true" aria-label="小挂件展开页"><div class="sheet-header"><h2>'+esc(item['name'])+'</h2><button class="close sheet-close" aria-label="关闭小挂件">'+icon('close')+'</button></div><div class="sheet-body">'+overview(sample, layout)
|
|
91
|
-
if 'tabs' in interactions or 'filter' in interactions:
|
|
92
|
-
options = sample['sections'] if 'tabs' in interactions else [{'id':'all','title':'全部'}]+sample['sections']
|
|
93
|
-
result += '<nav class="reading-nav" aria-label="筛选已有内容">'
|
|
94
|
-
for index, group in enumerate(options):
|
|
95
|
-
label = '<span class="day-number">'+str(index+1).zfill(2)+'</span><span>'+esc(group['title'])+'</span>' if layout == 'timeline' and 'tabs' in interactions else esc(group['title'])
|
|
96
|
-
result += '<button data-group="'+esc(group['id'])+'" aria-pressed="'+str(group['id']==selected).lower()+'">'+label+'</button>'
|
|
97
|
-
result += '</nav>'
|
|
98
|
-
for group in sample['sections']:
|
|
99
|
-
collapsible = 'collapse' in interactions
|
|
100
|
-
tag = 'details' if collapsible else 'section'; heading = 'summary' if collapsible else 'h3'
|
|
101
|
-
result += '<'+tag+' class="section" data-section="'+esc(group['id'])+'"'+(' open="open"' if collapsible else '')+(' hidden="hidden"' if selected!='all' and selected!=group['id'] else '')+'>'
|
|
102
|
-
result += '<'+heading+' class="section-heading">'+esc(group['title'])+(icon('chevron') if collapsible else '')+'</'+heading+'>'
|
|
103
|
-
result += '<div class="section-rows">'+''.join(row_html(row, layout) for row in group['rows'])+'</div></'+tag+'>'
|
|
104
|
-
result += '<p class="note">如需修改,请在对话中告诉搭子。</p></div></section><div class="detail-layer" hidden="hidden"><section class="detail-box" role="dialog" aria-modal="true" aria-label="只读详情"><div class="detail-head"><h3 class="detail-title">详情</h3><button class="close detail-close" aria-label="关闭详情">'+icon('close')+'</button></div><p class="detail-text"></p><p class="detail-foot">修改请通过对话完成</p></section></div></div></div>'
|
|
105
|
-
return result
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def files(artifact, item=None):
|
|
109
|
-
item = item or artifact.get('renderItem')
|
|
110
|
-
core.require(item, 'WIDGET_RENDER_ITEM', '生成原型需要对应清单项。')
|
|
111
|
-
data = artifact['data']; css = (ASSETS/'style.css').read_bytes(); js = (ASSETS/'app.js').read_bytes()
|
|
112
|
-
screens = ''.join(screen(data, item, sample) for sample in data['states'])
|
|
113
|
-
page = '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>'+esc(item['name'])+' · 小挂件原型</title><link rel="stylesheet" href="style.css"/></head><body>'+screens+'<script src="app.js"></script></body></html>'
|
|
114
|
-
result = {'index.html':page.encode('utf-8'), 'style.css':css, 'app.js':js}
|
|
115
|
-
w, h = data['design']['screenWidth'], data['design']['screenHeight']
|
|
116
|
-
for sample in data['states']:
|
|
117
|
-
for view in ('chat', 'expanded'):
|
|
118
|
-
content = screen(data, item, sample, view=='expanded')
|
|
119
|
-
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="'+str(w)+'" height="'+str(h)+'" viewBox="0 0 '+str(w)+' '+str(h)+'"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" class="svg-export" style="width:'+str(w)+'px;height:'+str(h)+'px"><style>'+html.escape(css.decode())+'</style>'+content+'</div></foreignObject></svg>'
|
|
120
|
-
result[view+'-'+sample['id']+'.svg'] = svg.encode('utf-8')
|
|
121
|
-
return result
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
"""Small read-only H5 designs, independently versioned after the four Booklets."""
|
|
2
|
-
import copy
|
|
3
|
-
import math
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
import buddy_core as core
|
|
7
|
-
|
|
8
|
-
KINDS = {'widget-catalog', 'widget-prototype', 'widget-acceptance'}
|
|
9
|
-
CATALOG = 'widget.catalog'
|
|
10
|
-
INTERACTIONS = {'tabs', 'filter', 'collapse', 'detail'}
|
|
11
|
-
STATES = ('initial', 'partial', 'updated')
|
|
12
|
-
REVIEW_CHECKS = ('booklet-fit', 'reading-order', 'chat-consistency', 'conversation-update', 'read-only')
|
|
13
|
-
PHASES = [('booklets', '四册完成'), ('catalog', '清单确认'), ('prototype', '原型确认'), ('acceptance', '设计验收'), ('complete', '交付')]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def enabled(state):
|
|
17
|
-
return state.get('widgetDesignVersion') == 1
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def books_ready(state):
|
|
21
|
-
return all(core.book_confirmed(state, s) for s in core.catalog()['stages'])
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def booklet_revision(state):
|
|
25
|
-
return core.digest({k: state['artifacts'].get(k, {}).get('hash') for s in core.catalog()['stages'] for k in core.book_ids(s)})
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def items(state):
|
|
29
|
-
return state['artifacts'].get(CATALOG, {}).get('data', {}).get('widgets', [])
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def item_hash(item):
|
|
33
|
-
return core.digest(item)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def object_id(key, kind='prototype'):
|
|
37
|
-
return 'widget.' + key + '.' + kind
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def schema(value, required, optional=()):
|
|
41
|
-
core.require(isinstance(value, dict) and set(required) <= set(value) and not set(value) - set(required) - set(optional),
|
|
42
|
-
'WIDGET_SCHEMA', '小挂件字段不完整或含未支持字段。', {'required': list(required), 'optional': list(optional)})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def strings(value, keys):
|
|
46
|
-
core.require(core.filled(value, keys), 'WIDGET_SCHEMA', '小挂件说明必须为非空文本。', {'fields': list(keys)})
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def validate(state, artifact):
|
|
50
|
-
core.require(enabled(state) and books_ready(state), 'WIDGET_STAGE', '先确认四册,再进入小挂件设计;旧作品须由开发者明确要求追加。')
|
|
51
|
-
core.require(artifact['stage'] == 'interaction' and artifact['kind'] in KINDS, 'WIDGET_SCHEMA', '小挂件使用独立 interaction 阶段。')
|
|
52
|
-
data, kind = artifact['data'], artifact['kind']
|
|
53
|
-
if kind == 'widget-catalog':
|
|
54
|
-
core.require(artifact['id'] == CATALOG, 'WIDGET_SCHEMA', '小挂件清单 ID 为 widget.catalog。')
|
|
55
|
-
schema(data, ('widgets',), ('noWidgetsReason',))
|
|
56
|
-
core.require(isinstance(data['widgets'], list), 'WIDGET_SCHEMA', 'widgets 必须是数组。')
|
|
57
|
-
core.require(data['widgets'] or core.text(data.get('noWidgetsReason')), 'WIDGET_EMPTY_REASON', '不使用小挂件也需说明依据,并展示给开发者明确确认。')
|
|
58
|
-
seen = set()
|
|
59
|
-
for item in data['widgets']:
|
|
60
|
-
fields = ('id', 'name', 'purpose', 'bookletRefs', 'creationTrigger', 'initialContent', 'updateRules', 'instancePolicy', 'conversationSummary', 'fields', 'readInteractions')
|
|
61
|
-
schema(item, fields)
|
|
62
|
-
strings(item, ('id', 'name', 'purpose', 'creationTrigger', 'initialContent', 'updateRules', 'instancePolicy', 'conversationSummary'))
|
|
63
|
-
core.require(re.fullmatch(r'[a-z][a-z0-9-]{0,39}', item['id']) and item['id'] not in seen, 'WIDGET_ID', '清单使用不重复的英文小写 ID,最长 40 位。')
|
|
64
|
-
seen.add(item['id'])
|
|
65
|
-
core.require(isinstance(item['readInteractions'], list) and len(set(item['readInteractions'])) == len(item['readInteractions']) and set(item['readInteractions']) <= INTERACTIONS,
|
|
66
|
-
'WIDGET_READ_ONLY', '小挂件只允许预设筛选、切换、折叠和只读详情;所有编辑、输入和业务动作通过对话完成。')
|
|
67
|
-
core.require(isinstance(item['fields'], list) and item['fields'], 'WIDGET_FIELDS', '说明对话逐步补充的展示字段。')
|
|
68
|
-
field_ids = set()
|
|
69
|
-
for field in item['fields']:
|
|
70
|
-
schema(field, ('key', 'label', 'source', 'emptyText'))
|
|
71
|
-
strings(field, ('key', 'label', 'source', 'emptyText'))
|
|
72
|
-
core.require(field['key'] not in field_ids, 'WIDGET_FIELDS', '字段 key 不能重复。')
|
|
73
|
-
field_ids.add(field['key'])
|
|
74
|
-
refs = item['bookletRefs']
|
|
75
|
-
core.require(isinstance(refs, list) and refs, 'WIDGET_BASIS', '每个小挂件需要绑定实际使用的 Booklet 章节版本。')
|
|
76
|
-
for ref in refs:
|
|
77
|
-
core.verify_ref(state, ref)
|
|
78
|
-
core.require(ref.get('type') == 'artifact' and state['artifacts'][ref['id']]['kind'] == 'chapter' and core.confirmed(state, ref['id']), 'WIDGET_BASIS', '依据必须为已确认的当前章节。')
|
|
79
|
-
# An empty list must still have an explicit Booklet basis.
|
|
80
|
-
core.require(any(r.get('type') == 'artifact' and state['artifacts'][r['id']]['kind'] == 'chapter' for r in artifact['dependencies']), 'WIDGET_BASIS', '清单需要引用已确认 Booklet。')
|
|
81
|
-
else:
|
|
82
|
-
core.require(core.confirmed(state, CATALOG) and not blockers(state, state['artifacts'][CATALOG]), 'WIDGET_CATALOG_UNCONFIRMED', '先展示并确认当前小挂件清单。')
|
|
83
|
-
key = data.get('widgetId')
|
|
84
|
-
item = next((i for i in items(state) if i['id'] == key), None)
|
|
85
|
-
core.require(item and data.get('itemHash') == item_hash(item), 'WIDGET_ITEM_STALE', '设计必须引用当前已确认清单中这一项的 itemHash。')
|
|
86
|
-
expected = object_id(key, 'prototype' if kind == 'widget-prototype' else 'acceptance')
|
|
87
|
-
core.require(artifact['id'] == expected, 'WIDGET_ID', '小挂件产物 ID 与清单项不一致。')
|
|
88
|
-
core.require(all(r in artifact['dependencies'] for r in item['bookletRefs']), 'WIDGET_BASIS', '保留该清单项实际使用的章节依赖。')
|
|
89
|
-
core.require(not any(r.get('id') == CATALOG for r in artifact['dependencies']), 'WIDGET_DEPENDENCY', '原型按 itemHash 绑定单项,不依赖整张清单,避免无关修改使全部原型失效。')
|
|
90
|
-
if kind == 'widget-prototype':
|
|
91
|
-
schema(data, ('widgetId', 'itemHash', 'rendererVersion', 'design', 'states'))
|
|
92
|
-
core.require(type(data['rendererVersion']) is int and data['rendererVersion'] in (1, 2, 3, 4), 'WIDGET_RENDERER_VERSION', '新原型使用模板版本 4,早期版本保留原样读取;视觉升级必须生成并确认新版本。')
|
|
93
|
-
visual = data['rendererVersion'] in (2, 3, 4)
|
|
94
|
-
design = data['design']
|
|
95
|
-
schema(design, ('accent', 'layout', 'titleSize', 'screenWidth', 'screenHeight') + (('icon', 'conversationTitle') if visual else ()))
|
|
96
|
-
layouts = ('list', 'cards', 'timeline', 'ledger', 'checklist') if visual else ('list', 'cards', 'timeline')
|
|
97
|
-
if data['rendererVersion'] == 4:
|
|
98
|
-
layouts += ('changes',)
|
|
99
|
-
core.require(isinstance(design['accent'], str) and re.fullmatch(r'#[0-9a-fA-F]{6}', design['accent']) and design['layout'] in layouts, 'WIDGET_STYLE', '样式使用六位十六进制强调色及支持的只读布局。')
|
|
100
|
-
if visual:
|
|
101
|
-
from widget_render_v2 import ICONS
|
|
102
|
-
strings(design, ('icon', 'conversationTitle'))
|
|
103
|
-
core.require(design['icon'] in ICONS, 'WIDGET_STYLE', '图标使用随包固定图标名称。')
|
|
104
|
-
core.require(type(design['titleSize']) is int and 16 <= design['titleSize'] <= 20 and type(design['screenWidth']) is int and 320 <= design['screenWidth'] <= 500 and type(design['screenHeight']) is int and 568 <= design['screenHeight'] <= 1100, 'WIDGET_SIZE', '标题建议 16 Medium,最大 20;画布用手机逻辑尺寸,正文固定 16 Regular。')
|
|
105
|
-
core.require(isinstance(data['states'], list) and [s.get('id') for s in data['states'] if isinstance(s, dict)] == list(STATES), 'WIDGET_STATES', '按 initial、partial、updated 提供首次生成、逐步补充、对话更新三态。')
|
|
106
|
-
for sample in data['states']:
|
|
107
|
-
schema(sample, ('id', 'label', 'userMessage', 'buddyMessage', 'summary', 'sections') + (('overview',) if visual else ()))
|
|
108
|
-
strings(sample, ('label', 'userMessage', 'buddyMessage', 'summary'))
|
|
109
|
-
if visual:
|
|
110
|
-
overview = sample['overview']
|
|
111
|
-
schema(overview, ('eyebrow', 'title', 'subtitle', 'metrics'), ('progress',))
|
|
112
|
-
strings(overview, ('eyebrow', 'title', 'subtitle'))
|
|
113
|
-
core.require(isinstance(overview['metrics'], list) and len(overview['metrics']) <= 3, 'WIDGET_CONTENT', '概览最多显示三个有依据的指标。')
|
|
114
|
-
for metric in overview['metrics']:
|
|
115
|
-
schema(metric, ('label', 'value'))
|
|
116
|
-
strings(metric, ('label', 'value'))
|
|
117
|
-
if 'progress' in overview:
|
|
118
|
-
p = overview['progress']
|
|
119
|
-
schema(p, ('value', 'max', 'label'))
|
|
120
|
-
strings(p, ('label',))
|
|
121
|
-
core.require(all(type(p[k]) in (int, float) and math.isfinite(p[k]) for k in ('value', 'max')) and p['max'] > 0 and 0 <= p['value'] <= p['max'], 'WIDGET_CONTENT', '进度只能展示有依据、有限且在范围内的数值。')
|
|
122
|
-
core.require(isinstance(sample['sections'], list) and sample['sections'], 'WIDGET_CONTENT', '每个状态需要实际内容或明确的缺项提示。')
|
|
123
|
-
section_ids = set()
|
|
124
|
-
for section in sample['sections']:
|
|
125
|
-
schema(section, ('id', 'title', 'rows'))
|
|
126
|
-
strings(section, ('id', 'title'))
|
|
127
|
-
core.require(section['id'] not in section_ids, 'WIDGET_CONTENT', '分组 ID 不能重复。')
|
|
128
|
-
section_ids.add(section['id'])
|
|
129
|
-
core.require(isinstance(section['rows'], list) and section['rows'], 'WIDGET_CONTENT', '分组需要阅读内容。')
|
|
130
|
-
for row in section['rows']:
|
|
131
|
-
optional = ('detail', 'caption', 'status', 'icon') if visual else ('detail',)
|
|
132
|
-
if data['rendererVersion'] == 4:
|
|
133
|
-
optional += ('change',)
|
|
134
|
-
schema(row, ('label', 'value'), optional)
|
|
135
|
-
strings(row, ('label', 'value'))
|
|
136
|
-
core.require('detail' not in row or core.text(row['detail']) and 'detail' in item['readInteractions'], 'WIDGET_READ_ONLY', '只读详情须在清单中约定。')
|
|
137
|
-
if visual:
|
|
138
|
-
for key in ('caption', 'icon'):
|
|
139
|
-
if key in row:
|
|
140
|
-
strings(row, (key,))
|
|
141
|
-
core.require('icon' not in row or row['icon'] in ICONS, 'WIDGET_STYLE', '图标使用随包固定图标名称。')
|
|
142
|
-
if 'status' in row:
|
|
143
|
-
schema(row['status'], ('label', 'tone'))
|
|
144
|
-
strings(row['status'], ('label', 'tone'))
|
|
145
|
-
core.require(row['status']['tone'] in ('neutral', 'pending', 'positive', 'changed'), 'WIDGET_STYLE', '状态色仅表达中性、待补、已确认或变化,不承载业务动作。')
|
|
146
|
-
if 'change' in row:
|
|
147
|
-
core.require(design['layout'] == 'changes' and row.get('status', {}).get('tone') in ('pending', 'changed'), 'WIDGET_CHANGE', '前后对比只用于 changes 布局,并明确为待确认建议或已生效变化。')
|
|
148
|
-
schema(row['change'], ('before', 'after'))
|
|
149
|
-
strings(row['change'], ('before', 'after'))
|
|
150
|
-
else:
|
|
151
|
-
schema(data, ('widgetId', 'itemHash', 'prototypeHash', 'reviews'))
|
|
152
|
-
prototype = state['artifacts'].get(object_id(key))
|
|
153
|
-
core.require(prototype and core.confirmed(state, prototype['id']) and not blockers(state, prototype) and data['prototypeHash'] == prototype['hash'], 'WIDGET_PROTOTYPE_UNCONFIRMED', '先确认当前原型,再提交该版本的设计验收。')
|
|
154
|
-
core.require({'type': 'artifact', 'id': prototype['id'], 'hash': prototype['hash']} in artifact['dependencies'], 'WIDGET_DEPENDENCY', '验收必须绑定原型确切版本。')
|
|
155
|
-
reviews = data['reviews']
|
|
156
|
-
schema(reviews, REVIEW_CHECKS)
|
|
157
|
-
strings(reviews, REVIEW_CHECKS)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def blockers(state, artifact):
|
|
161
|
-
result = {}
|
|
162
|
-
if artifact.get('unresolved'):
|
|
163
|
-
result['unresolved'] = artifact['unresolved']
|
|
164
|
-
if not books_ready(state):
|
|
165
|
-
result['booklets'] = '四册当前版本尚未全部确认'
|
|
166
|
-
for ref in artifact.get('dependencies', []):
|
|
167
|
-
if ref.get('type') == 'artifact':
|
|
168
|
-
current = state['artifacts'].get(ref['id'], {})
|
|
169
|
-
if current.get('hash') != ref['hash'] or not core.confirmed(state, ref['id']):
|
|
170
|
-
result.setdefault('dependencies', []).append(ref['id'])
|
|
171
|
-
if artifact['kind'] == 'widget-catalog':
|
|
172
|
-
for item in artifact['data']['widgets']:
|
|
173
|
-
for ref in item['bookletRefs']:
|
|
174
|
-
if state['artifacts'].get(ref['id'], {}).get('hash') != ref['hash'] or not core.confirmed(state, ref['id']):
|
|
175
|
-
result.setdefault('items', []).append(item['id'])
|
|
176
|
-
else:
|
|
177
|
-
item = next((i for i in items(state) if i['id'] == artifact['data']['widgetId']), None)
|
|
178
|
-
if not core.confirmed(state, CATALOG) or blockers(state, state['artifacts'][CATALOG]):
|
|
179
|
-
result['catalog'] = '当前清单待确认'
|
|
180
|
-
if not item or item_hash(item) != artifact['data']['itemHash']:
|
|
181
|
-
result['item'] = '清单项已变更或移除'
|
|
182
|
-
return result
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def valid_confirmation(state, key):
|
|
186
|
-
artifact = state['artifacts'].get(key)
|
|
187
|
-
return bool(artifact and core.confirmed(state, key) and not blockers(state, artifact))
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def phase(state):
|
|
191
|
-
if not enabled(state):
|
|
192
|
-
return 'legacy'
|
|
193
|
-
if not books_ready(state):
|
|
194
|
-
return 'booklets'
|
|
195
|
-
if not valid_confirmation(state, CATALOG):
|
|
196
|
-
return 'catalog'
|
|
197
|
-
if any(not valid_confirmation(state, object_id(i['id'])) for i in items(state)):
|
|
198
|
-
return 'prototype'
|
|
199
|
-
if any(not valid_confirmation(state, object_id(i['id'], 'acceptance')) for i in items(state)):
|
|
200
|
-
return 'acceptance'
|
|
201
|
-
return 'complete'
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def ready(state):
|
|
205
|
-
return not enabled(state) or phase(state) == 'complete'
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def projection(state, workspace=None):
|
|
209
|
-
from widget_render import asset_status
|
|
210
|
-
current = phase(state)
|
|
211
|
-
design_items = []
|
|
212
|
-
for item in items(state):
|
|
213
|
-
key = object_id(item['id'])
|
|
214
|
-
prototype = state['artifacts'].get(key)
|
|
215
|
-
acceptance = state['artifacts'].get(object_id(item['id'], 'acceptance'))
|
|
216
|
-
checks = asset_status(workspace, prototype) if workspace and prototype else {'pass': False, 'message': '原型尚未生成'}
|
|
217
|
-
design_items.append({**copy.deepcopy(item), 'itemHash': item_hash(item),
|
|
218
|
-
'prototype': copy.deepcopy(prototype), 'prototypeConfirmed': valid_confirmation(state, key),
|
|
219
|
-
'acceptance': copy.deepcopy(acceptance), 'accepted': valid_confirmation(state, object_id(item['id'], 'acceptance')),
|
|
220
|
-
'blockers': blockers(state, prototype) if prototype else {}, 'automaticCheck': checks})
|
|
221
|
-
next_step = {'legacy': '旧作品沿用原交付流程;开发者明确要求后可追加小挂件设计。', 'booklets': '先完成并确认四册,已有内容可随时阅读。',
|
|
222
|
-
'catalog': '根据四册提出小挂件清单,在主对话确认、补充或明确不需要。', 'prototype': '查看每项的对话卡片、展开页及三种数据状态,在主对话确认或调整。',
|
|
223
|
-
'acceptance': '检查阅读体验、只读边界和对话更新关系,在主对话确认当前版本的设计验收。', 'complete': '设计确认已完成,检查交付文件后继续开发。'}[current]
|
|
224
|
-
return {'enabled': enabled(state), 'phase': current, 'steps': [{'id': key, 'label': label} for key, label in PHASES],
|
|
225
|
-
'bookletRevision': booklet_revision(state), 'nextStep': next_step, 'catalog': copy.deepcopy(state['artifacts'].get(CATALOG)),
|
|
226
|
-
'catalogConfirmed': valid_confirmation(state, CATALOG), 'items': design_items,
|
|
227
|
-
'reviewChecks': list(REVIEW_CHECKS), 'spec': {'widthInset': 32, 'heightRatio': .65, 'titleRecommended': 16, 'titleMax': 20, 'bodySize': 16, 'readOnly': True}}
|