dsh-grove 0.2.0 → 0.5.1
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.en.md +34 -74
- package/README.md +35 -77
- package/client.js +799 -196
- package/package.json +4 -2
- package/src/adapters/models/http.js +24 -0
- package/src/adapters/parsers/documents.js +31 -0
- package/src/adapters/storage/obsidian/backup.js +104 -0
- package/src/cli.js +11 -3
- package/src/modules/evidence.js +38 -0
- package/src/modules/inspirations.js +18 -0
- package/src/modules/knowledge.js +29 -0
- package/src/modules/model-research.js +27 -0
- package/src/modules/reports.js +57 -0
- package/src/modules/research.js +17 -0
- package/src/modules/version-reference.js +4 -0
- package/src/modules/works.js +27 -0
- package/src/plugin.js +10 -2
- package/src/proposals.js +1 -1
- package/src/server.js +234 -2
- package/src/vault.js +16 -1
package/client.js
CHANGED
|
@@ -19,55 +19,101 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
|
|
20
20
|
var NS = 'grove'
|
|
21
21
|
var STATUS = '/api/grove/status'
|
|
22
|
+
var CONNECTION = '/api/grove/connection'
|
|
22
23
|
var CONFIG = '/api/grove/config'
|
|
23
24
|
var CAPTURE = '/api/grove/capture'
|
|
25
|
+
var KNOWLEDGE = '/api/grove/knowledge'
|
|
26
|
+
var WORKS = '/api/grove/works'
|
|
27
|
+
var MODEL = '/api/grove/model'
|
|
28
|
+
var FINDINGS = '/api/grove/findings'
|
|
29
|
+
var PARSERS = '/api/grove/parsers'
|
|
30
|
+
var REPORTS = '/api/grove/reports'
|
|
31
|
+
var SOURCES = '/api/grove/sources'
|
|
32
|
+
var RESEARCH = '/api/grove/research'
|
|
33
|
+
var INSPIRATIONS = '/api/grove/inspirations'
|
|
24
34
|
|
|
25
35
|
var ZH = {
|
|
36
|
+
's0.pickerUnavailable': '这个宿主暂时无法打开文件夹选择器。可以手动输入路径。',
|
|
37
|
+
's0.pickerFailed': '没能打开文件夹选择器。{message}',
|
|
38
|
+
'asset.knowledge': '知识库',
|
|
39
|
+
'asset.works': '作品',
|
|
40
|
+
'asset.title': '标题',
|
|
41
|
+
'asset.body': '正文',
|
|
42
|
+
'asset.new': '新建',
|
|
43
|
+
'asset.save': '保存',
|
|
44
|
+
'asset.basis': '查看依据',
|
|
45
|
+
'asset.derive': '提炼为知识',
|
|
46
|
+
'asset.compose': '写成作品',
|
|
47
|
+
'asset.research': '带着这条知识深挖',
|
|
48
|
+
'asset.reason': '实践反思 · 什么改变了你的理解?',
|
|
49
|
+
'asset.copy': '复制正文',
|
|
50
|
+
'asset.copied': '已复制',
|
|
51
|
+
'asset.type': '知识类型',
|
|
52
|
+
'asset.missing': '来源不可用。',
|
|
53
|
+
'report.review': '审核为报告草稿',
|
|
54
|
+
'report.discard': '丢弃未保存草稿',
|
|
55
|
+
'model.title': '研究模型',
|
|
56
|
+
'model.enabled': '启用 AI 深挖',
|
|
57
|
+
'model.endpoint': 'HTTPS 模型接口',
|
|
58
|
+
'model.name': '模型名称',
|
|
59
|
+
'model.test': '测试连接',
|
|
60
|
+
'model.apply': '应用模型设置',
|
|
61
|
+
'model.tested': '连接通过。',
|
|
62
|
+
'model.ask': '请 AI 深挖',
|
|
63
|
+
'model.question': '想往哪儿深挖?',
|
|
64
|
+
'model.scope': '仅发送勾选的摘录和本研究先前的发现。',
|
|
65
|
+
'parsers.title': '资料解析设置',
|
|
66
|
+
'parsers.pdf': '启用文本 PDF 解析',
|
|
67
|
+
'parsers.apply': '应用设置',
|
|
68
|
+
'parsers.pending': '正在应用…',
|
|
69
|
+
'parsers.done': '解析设置已应用。',
|
|
70
|
+
'parsers.failed': '设置还没生效,请重试。',
|
|
71
|
+
'parsers.hint': 'Markdown 始终可导入。关闭 PDF 解析后,旧证据仍可回看。',
|
|
72
|
+
'report.title': '研究报告',
|
|
73
|
+
'report.new': '新报告',
|
|
74
|
+
'report.body': '报告正文',
|
|
75
|
+
'report.save': '确认报告',
|
|
76
|
+
'report.history': '历史版本',
|
|
77
|
+
'report.missing': '原件不可用,仍可核对上面的摘录。',
|
|
78
|
+
'report.missingEvidence': '这条证据已不可用。',
|
|
79
|
+
'source.title': '研究资料',
|
|
80
|
+
'source.import': '导入 Markdown 或文本 PDF',
|
|
81
|
+
'source.read': '看看原文',
|
|
82
|
+
'source.save': '保存为证据',
|
|
83
|
+
'source.saved': '证据已保存',
|
|
84
|
+
'source.open': '打开原始文件',
|
|
85
|
+
'research.title': '研究',
|
|
86
|
+
'research.new': '新建独立研究',
|
|
87
|
+
'research.question': '想深挖什么?',
|
|
88
|
+
'research.notes': '研究记录',
|
|
89
|
+
'research.save': '保存研究',
|
|
90
|
+
'research.continue': '接着上次深挖',
|
|
91
|
+
'research.back': '返回首页',
|
|
92
|
+
'research.saved': '已保存。',
|
|
93
|
+
'idea.title': '灵感来了',
|
|
94
|
+
'idea.placeholder': '刚冒出什么点子?先记一句。',
|
|
95
|
+
'idea.save': '💡叮~',
|
|
96
|
+
'idea.saved': '叮~ 记住啦!',
|
|
97
|
+
'idea.find': '找回灵感',
|
|
98
|
+
'idea.search': '搜索',
|
|
99
|
+
'idea.empty': '没找到这条灵感。',
|
|
100
|
+
'idea.version': '历史版本',
|
|
26
101
|
panel: 'Grove',
|
|
27
102
|
menu: '知林',
|
|
28
|
-
subtitle: '
|
|
103
|
+
subtitle: '记灵感,做研究',
|
|
29
104
|
'status.connected': '已连接',
|
|
30
105
|
'status.disconnected': '未连接',
|
|
31
106
|
'status.readonly': '只读',
|
|
32
107
|
'status.checking': '正在读取…',
|
|
33
|
-
's0.title': '
|
|
34
|
-
's0.body': '
|
|
35
|
-
's0.step1': '连接 vault',
|
|
36
|
-
's0.step2': '写下第一条灵感',
|
|
37
|
-
's0.step3': '回到会话继续探索',
|
|
108
|
+
's0.title': '连接 Obsidian 笔记库',
|
|
109
|
+
's0.body': '选一下你在 Obsidian 里打开的笔记库文件夹,连好就能记下第一个灵感。',
|
|
38
110
|
's0.pick': '选择文件夹',
|
|
39
111
|
's0.manual': '手动输入路径',
|
|
40
112
|
's0.placeholder': '/Users/你/Obsidian Vault',
|
|
41
113
|
's0.connect': '连接',
|
|
42
114
|
's0.cancel': '取消',
|
|
43
|
-
's0.hint': '路径会保存到当前 profile 的插件配置;之后可在插件页里修改。',
|
|
44
115
|
's0.stale': '原来的 vault 已不可用:{path}。请重新选择,或把它移回原位。',
|
|
45
|
-
's0.noVault': '这个文件夹里没有 .obsidian。如果它不是 vault 根目录,仍可继续连接。',
|
|
46
|
-
's1.connectedTo': '已连接',
|
|
47
116
|
's1.change': '更换 vault',
|
|
48
|
-
's1.title': '写下第一条灵感',
|
|
49
|
-
's1.body': '一个观察、一个困惑、一句还没想清楚的话都可以。保存后它会出现在 vault 的 Grove/ 文件夹里。',
|
|
50
|
-
's1.placeholder': '比如:为什么组织扩大后,协调成本会上升?',
|
|
51
|
-
's1.save': '保存到 vault',
|
|
52
|
-
's1.saving': '正在保存…',
|
|
53
|
-
's1.saved': '已保存',
|
|
54
|
-
's1.savedBody': '已写入 vault。用 Obsidian 打开 Grove/ 文件夹就能看到。回到会话里,让 AI 用 grove_role 陪你继续想。',
|
|
55
|
-
's1.view': '查看概览',
|
|
56
|
-
's1.another': '另一种开始',
|
|
57
|
-
's1.prompt': '用 grove_role 当我的思想伙伴,先帮我拆开这个问题:<你的问题>',
|
|
58
|
-
's1.copy': '复制提示词',
|
|
59
|
-
's1.copied': '已复制',
|
|
60
|
-
's2.title': '你的知识库',
|
|
61
|
-
's2.total': '条资产',
|
|
62
|
-
's2.recent': '最近更新',
|
|
63
|
-
's2.empty': '还没有笔记。',
|
|
64
|
-
's2.continue': '在会话里继续探索',
|
|
65
|
-
's2.prompt': '用 grove_role 当我的思想伙伴,接着上次的问题继续。',
|
|
66
|
-
's2.count.inspiration': '灵感',
|
|
67
|
-
's2.count.question': '问题',
|
|
68
|
-
's2.count.research': '研究',
|
|
69
|
-
's2.count.claim': '判断',
|
|
70
|
-
's2.count.evidence': '证据',
|
|
71
117
|
'type.inspiration': '灵感',
|
|
72
118
|
'type.question': '问题',
|
|
73
119
|
'type.concept': '概念',
|
|
@@ -78,9 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
78
124
|
'type.output': '成果',
|
|
79
125
|
'type.exploration': '探索',
|
|
80
126
|
'type.research': '研究',
|
|
81
|
-
'foot.privacy': '
|
|
82
|
-
'foot.confirm': 'AI 起草的草稿要你在终端确认后才落盘。',
|
|
83
|
-
'foot.data': '数据是你的 Markdown,随时可以备份或迁移。',
|
|
127
|
+
'foot.privacy': '灵感和研究保存在笔记库的 Grove/ 文件夹里。',
|
|
84
128
|
'err.invalid-path': '路径无效,请填写绝对路径。',
|
|
85
129
|
'err.not-directory': '这个路径不是文件夹。',
|
|
86
130
|
'err.symlink': '这个路径是符号链接,请选择真实目录。',
|
|
@@ -97,51 +141,87 @@ window.__ModuleLoader__.load({
|
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
var EN = {
|
|
144
|
+
's0.pickerUnavailable': 'This host cannot open a folder picker. You can enter the path manually.',
|
|
145
|
+
's0.pickerFailed': 'Could not open the folder picker. {message}',
|
|
146
|
+
'asset.knowledge': 'Knowledge',
|
|
147
|
+
'asset.works': 'Works',
|
|
148
|
+
'asset.title': 'Title',
|
|
149
|
+
'asset.body': 'Body',
|
|
150
|
+
'asset.new': 'New',
|
|
151
|
+
'asset.save': 'Save',
|
|
152
|
+
'asset.basis': 'View basis',
|
|
153
|
+
'asset.derive': 'Save as knowledge',
|
|
154
|
+
'asset.compose': 'Create a work',
|
|
155
|
+
'asset.research': 'Research from this knowledge',
|
|
156
|
+
'asset.reason': 'Reflection · What changed your understanding?',
|
|
157
|
+
'asset.copy': 'Copy body',
|
|
158
|
+
'asset.copied': 'Copied',
|
|
159
|
+
'asset.type': 'Knowledge type',
|
|
160
|
+
'asset.missing': 'The source is unavailable.',
|
|
161
|
+
'report.review': 'Review as report draft',
|
|
162
|
+
'report.discard': 'Discard unsaved draft',
|
|
163
|
+
'model.title': 'Research model',
|
|
164
|
+
'model.enabled': 'Enable AI research',
|
|
165
|
+
'model.endpoint': 'HTTPS model endpoint',
|
|
166
|
+
'model.name': 'Model name',
|
|
167
|
+
'model.test': 'Test connection',
|
|
168
|
+
'model.apply': 'Apply model settings',
|
|
169
|
+
'model.tested': 'Connection verified.',
|
|
170
|
+
'model.ask': 'Ask AI to investigate',
|
|
171
|
+
'model.question': 'What should we investigate?',
|
|
172
|
+
'model.scope': 'Only selected excerpts and earlier findings in this study are sent.',
|
|
173
|
+
'parsers.title': 'Parser settings',
|
|
174
|
+
'parsers.pdf': 'Enable text PDF parsing',
|
|
175
|
+
'parsers.apply': 'Apply settings',
|
|
176
|
+
'parsers.pending': 'Applying…',
|
|
177
|
+
'parsers.done': 'Parser settings applied.',
|
|
178
|
+
'parsers.failed': 'Settings did not take effect. Try again.',
|
|
179
|
+
'parsers.hint': 'Markdown remains available. Existing evidence stays readable when PDF parsing is off.',
|
|
180
|
+
'report.title': 'Research reports',
|
|
181
|
+
'report.new': 'New report',
|
|
182
|
+
'report.body': 'Report body',
|
|
183
|
+
'report.save': 'Confirm report',
|
|
184
|
+
'report.history': 'History',
|
|
185
|
+
'report.missing': 'Original unavailable. The saved excerpt is shown above.',
|
|
186
|
+
'report.missingEvidence': 'This evidence version is unavailable.',
|
|
187
|
+
'source.title': 'Sources',
|
|
188
|
+
'source.import': 'Import Markdown or text PDF',
|
|
189
|
+
'source.read': 'View original passage',
|
|
190
|
+
'source.save': 'Save as evidence',
|
|
191
|
+
'source.saved': 'Evidence saved',
|
|
192
|
+
'source.open': 'Open original file',
|
|
193
|
+
'research.title': 'Research',
|
|
194
|
+
'research.new': 'New independent study',
|
|
195
|
+
'research.question': 'What do you want to explore?',
|
|
196
|
+
'research.notes': 'Research notes',
|
|
197
|
+
'research.save': 'Save study',
|
|
198
|
+
'research.continue': 'Continue the last study',
|
|
199
|
+
'research.back': 'Back to home',
|
|
200
|
+
'research.saved': 'Saved.',
|
|
201
|
+
'idea.title': 'An idea?',
|
|
202
|
+
'idea.placeholder': 'What just came to mind? Jot it down.',
|
|
203
|
+
'idea.save': '💡 Ding!',
|
|
204
|
+
'idea.saved': 'Ding! Saved.',
|
|
205
|
+
'idea.find': 'Find ideas',
|
|
206
|
+
'idea.search': 'Search',
|
|
207
|
+
'idea.empty': 'No matching ideas.',
|
|
208
|
+
'idea.version': 'History',
|
|
100
209
|
panel: 'Grove',
|
|
101
210
|
menu: 'Grove',
|
|
102
|
-
subtitle: '
|
|
211
|
+
subtitle: 'Capture ideas and do research',
|
|
103
212
|
'status.connected': 'Connected',
|
|
104
213
|
'status.disconnected': 'Not connected',
|
|
105
214
|
'status.readonly': 'Read only',
|
|
106
215
|
'status.checking': 'Reading…',
|
|
107
216
|
's0.title': 'Connect your Obsidian vault',
|
|
108
|
-
's0.body': '
|
|
109
|
-
's0.step1': 'Connect a vault',
|
|
110
|
-
's0.step2': 'Capture a first idea',
|
|
111
|
-
's0.step3': 'Go back to the conversation',
|
|
217
|
+
's0.body': 'Choose the folder you open as a vault in Obsidian. Once it’s connected, you can jot down your first idea.',
|
|
112
218
|
's0.pick': 'Choose folder',
|
|
113
219
|
's0.manual': 'Enter a path',
|
|
114
220
|
's0.placeholder': '/Users/you/Obsidian Vault',
|
|
115
221
|
's0.connect': 'Connect',
|
|
116
222
|
's0.cancel': 'Cancel',
|
|
117
|
-
's0.hint': 'The path is saved in this profile’s plugin configuration and can be changed later from the Plugins page.',
|
|
118
223
|
's0.stale': 'The previous vault is no longer usable: {path}. Choose it again, or move it back.',
|
|
119
|
-
's0.noVault': 'This folder has no .obsidian. You can still connect if it is a vault root.',
|
|
120
|
-
's1.connectedTo': 'Connected',
|
|
121
224
|
's1.change': 'Change vault',
|
|
122
|
-
's1.title': 'Capture a first idea',
|
|
123
|
-
's1.body': 'An observation, a question, or a thought you have not worked out yet. Saving puts it in the vault’s Grove/ folder.',
|
|
124
|
-
's1.placeholder': 'For example: why do coordination costs rise as an organization grows?',
|
|
125
|
-
's1.save': 'Save to vault',
|
|
126
|
-
's1.saving': 'Saving…',
|
|
127
|
-
's1.saved': 'Saved',
|
|
128
|
-
's1.savedBody': 'Written to the vault. Open the Grove/ folder in Obsidian to see it. Back in the conversation, ask AI to use grove_role and keep thinking.',
|
|
129
|
-
's1.view': 'View overview',
|
|
130
|
-
's1.another': 'Another way to start',
|
|
131
|
-
's1.prompt': 'Use grove_role as my thinking partner and help me unpack this question: <your question>',
|
|
132
|
-
's1.copy': 'Copy prompt',
|
|
133
|
-
's1.copied': 'Copied',
|
|
134
|
-
's2.title': 'Your knowledge base',
|
|
135
|
-
's2.total': 'assets',
|
|
136
|
-
's2.recent': 'Recently updated',
|
|
137
|
-
's2.empty': 'No notes yet.',
|
|
138
|
-
's2.continue': 'Continue in the conversation',
|
|
139
|
-
's2.prompt': 'Use grove_role as my thinking partner and continue from the last question.',
|
|
140
|
-
's2.count.inspiration': 'Inspirations',
|
|
141
|
-
's2.count.question': 'Questions',
|
|
142
|
-
's2.count.research': 'Research',
|
|
143
|
-
's2.count.claim': 'Claims',
|
|
144
|
-
's2.count.evidence': 'Evidence',
|
|
145
225
|
'type.inspiration': 'Inspiration',
|
|
146
226
|
'type.question': 'Question',
|
|
147
227
|
'type.concept': 'Concept',
|
|
@@ -152,9 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
152
232
|
'type.output': 'Output',
|
|
153
233
|
'type.exploration': 'Exploration',
|
|
154
234
|
'type.research': 'Research',
|
|
155
|
-
'foot.privacy': '
|
|
156
|
-
'foot.confirm': 'Content drafted by AI is saved only after you confirm it in the terminal.',
|
|
157
|
-
'foot.data': 'Your data is Markdown you can back up or move at any time.',
|
|
235
|
+
'foot.privacy': 'Your ideas and research are saved in the Grove/ folder inside your vault.',
|
|
158
236
|
'err.invalid-path': 'That path is not valid. Use an absolute path.',
|
|
159
237
|
'err.not-directory': 'That path is not a folder.',
|
|
160
238
|
'err.symlink': 'That path is a symlink. Choose a real directory.',
|
|
@@ -171,7 +249,7 @@ window.__ModuleLoader__.load({
|
|
|
171
249
|
}
|
|
172
250
|
|
|
173
251
|
var boundT = null
|
|
174
|
-
var services = {
|
|
252
|
+
var services = { picker: null }
|
|
175
253
|
|
|
176
254
|
function T(key, vars) {
|
|
177
255
|
if (boundT) {
|
|
@@ -214,18 +292,6 @@ window.__ModuleLoader__.load({
|
|
|
214
292
|
return (error && error.message) || T('err.internal')
|
|
215
293
|
}
|
|
216
294
|
|
|
217
|
-
function when(iso) {
|
|
218
|
-
if (!iso) return ''
|
|
219
|
-
var date = new Date(iso)
|
|
220
|
-
if (isNaN(date.getTime())) return ''
|
|
221
|
-
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function copyText(text) {
|
|
225
|
-
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text)
|
|
226
|
-
return Promise.reject(new Error('clipboard'))
|
|
227
|
-
}
|
|
228
|
-
|
|
229
295
|
function waitForVault(expected, tries) {
|
|
230
296
|
return api(STATUS).catch(function () { return null }).then(function (status) {
|
|
231
297
|
if (status && status.configured && status.usable && (expected === undefined || status.vault === expected)) return status
|
|
@@ -249,9 +315,18 @@ window.__ModuleLoader__.load({
|
|
|
249
315
|
var [manual, setManual] = useState(false)
|
|
250
316
|
var [draft, setDraft] = useState('')
|
|
251
317
|
var [pathDraft, setPathDraft] = useState('')
|
|
252
|
-
var [
|
|
253
|
-
var [
|
|
318
|
+
var [query, setQuery] = useState('')
|
|
319
|
+
var [notes, setNotes] = useState(null)
|
|
320
|
+
var [selected, setSelected] = useState(null)
|
|
321
|
+
var [saved, setSaved] = useState(false)
|
|
254
322
|
var [changing, setChanging] = useState(false)
|
|
323
|
+
var [page, setPage] = useState('home')
|
|
324
|
+
var [seed, setSeed] = useState(null)
|
|
325
|
+
|
|
326
|
+
function navigate(destination, draft) {
|
|
327
|
+
setSeed(draft || null)
|
|
328
|
+
setPage(destination)
|
|
329
|
+
}
|
|
255
330
|
var mounted = useRef(true)
|
|
256
331
|
|
|
257
332
|
useEffect(function () {
|
|
@@ -277,11 +352,17 @@ window.__ModuleLoader__.load({
|
|
|
277
352
|
function connect(path) {
|
|
278
353
|
setBusy('connect')
|
|
279
354
|
setError(null)
|
|
280
|
-
return api(
|
|
281
|
-
return
|
|
355
|
+
return api(CONNECTION, { path: path }).then(function (candidate) {
|
|
356
|
+
return api(CONFIG, { path: candidate.path }).then(function () {
|
|
357
|
+
return waitForVault(candidate.path, 12)
|
|
358
|
+
})
|
|
282
359
|
}).then(function (next) {
|
|
283
360
|
if (!mounted.current) return
|
|
284
361
|
setStatus(next)
|
|
362
|
+
navigate('home')
|
|
363
|
+
setNotes(null)
|
|
364
|
+
setSelected(null)
|
|
365
|
+
setSaved(false)
|
|
285
366
|
setManual(false)
|
|
286
367
|
setChanging(false)
|
|
287
368
|
}, function (problem) {
|
|
@@ -292,25 +373,26 @@ window.__ModuleLoader__.load({
|
|
|
292
373
|
})
|
|
293
374
|
}
|
|
294
375
|
|
|
295
|
-
function pickFolder() {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
376
|
+
async function pickFolder() {
|
|
377
|
+
if (busy) return
|
|
378
|
+
var picker = services.picker
|
|
379
|
+
if (!picker) {
|
|
380
|
+
setError(new Error(T('s0.pickerUnavailable')))
|
|
299
381
|
return
|
|
300
382
|
}
|
|
383
|
+
setError(null)
|
|
301
384
|
setBusy('pick')
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
385
|
+
var path
|
|
386
|
+
try {
|
|
387
|
+
var result = await picker.pick()
|
|
388
|
+
if (!result.ok) throw new Error(result.error.message)
|
|
389
|
+
path = result.value
|
|
390
|
+
} catch (problem) {
|
|
391
|
+
if (mounted.current) setError(new Error(T('s0.pickerFailed', { message: messageOf(problem) })))
|
|
392
|
+
} finally {
|
|
307
393
|
if (mounted.current) setBusy('')
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!mounted.current) return
|
|
311
|
-
setBusy('')
|
|
312
|
-
setManual(true)
|
|
313
|
-
})
|
|
394
|
+
}
|
|
395
|
+
if (mounted.current && path) return connect(path)
|
|
314
396
|
}
|
|
315
397
|
|
|
316
398
|
function submitManual() {
|
|
@@ -319,37 +401,39 @@ window.__ModuleLoader__.load({
|
|
|
319
401
|
connect(path)
|
|
320
402
|
}
|
|
321
403
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
setBusy('capture')
|
|
404
|
+
function requestNotes(input, apply) {
|
|
405
|
+
if (busy) return
|
|
406
|
+
setBusy('read')
|
|
326
407
|
setError(null)
|
|
327
|
-
api(
|
|
328
|
-
if (
|
|
329
|
-
setFirstNote(note)
|
|
330
|
-
setDraft('')
|
|
331
|
-
return api(STATUS).then(function (next) {
|
|
332
|
-
if (mounted.current) setStatus(next)
|
|
333
|
-
})
|
|
408
|
+
return api(INSPIRATIONS, input).then(function (data) {
|
|
409
|
+
if (mounted.current) apply(data)
|
|
334
410
|
}, function (problem) {
|
|
335
411
|
if (mounted.current) setError(problem)
|
|
336
|
-
}).
|
|
412
|
+
}).finally(function () {
|
|
337
413
|
if (mounted.current) setBusy('')
|
|
338
414
|
})
|
|
339
415
|
}
|
|
340
416
|
|
|
341
|
-
function
|
|
342
|
-
|
|
417
|
+
function saveIdea() {
|
|
418
|
+
var body = draft.trim()
|
|
419
|
+
if (!body || busy) return
|
|
420
|
+
setBusy('capture')
|
|
421
|
+
setError(null)
|
|
422
|
+
setSaved(false)
|
|
423
|
+
api(CAPTURE, { body: body }).then(function () {
|
|
343
424
|
if (!mounted.current) return
|
|
344
|
-
|
|
345
|
-
|
|
425
|
+
setDraft('')
|
|
426
|
+
setSaved(true)
|
|
427
|
+
setNotes(null)
|
|
428
|
+
setSelected(null)
|
|
346
429
|
}, function (problem) {
|
|
347
430
|
if (mounted.current) setError(problem)
|
|
431
|
+
}).finally(function () {
|
|
432
|
+
if (mounted.current) setBusy('')
|
|
348
433
|
})
|
|
349
434
|
}
|
|
350
435
|
|
|
351
436
|
var connected = !!(status && status.configured && status.usable)
|
|
352
|
-
var total = status && status.counts ? Number(status.counts.total) || 0 : 0
|
|
353
437
|
var connectView = h(Connect, {
|
|
354
438
|
busy: busy, manual: manual, error: error, pathDraft: pathDraft,
|
|
355
439
|
stale: !changing && status && status.configured && !status.usable ? status.vault : null,
|
|
@@ -376,12 +460,23 @@ window.__ModuleLoader__.load({
|
|
|
376
460
|
? h('div', { className: 'gr-card' }, h('div', { className: 'gr-body' }, T('loading')))
|
|
377
461
|
: changing || !connected
|
|
378
462
|
? connectView
|
|
379
|
-
:
|
|
380
|
-
? h(
|
|
381
|
-
:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
463
|
+
: page === 'research'
|
|
464
|
+
? h(ResearchPage, { seed: seed, onNavigate: navigate, onBack: function () { navigate('home') } })
|
|
465
|
+
: page === 'knowledge' || page === 'works'
|
|
466
|
+
? h(AssetPage, { key: page, kind: page, seed: seed, onNavigate: navigate })
|
|
467
|
+
: h(InspirationHome, {
|
|
468
|
+
onResearch: function () { navigate('research') },
|
|
469
|
+
onKnowledge: function () { navigate('knowledge') },
|
|
470
|
+
onWorks: function () { navigate('works') },
|
|
471
|
+
draft: draft, busy: busy, saved: saved, onSave: saveIdea,
|
|
472
|
+
onDraft: function (value) { setDraft(value); setSaved(false) },
|
|
473
|
+
query: query, notes: notes, selected: selected, onQuery: setQuery,
|
|
474
|
+
onSearch: function () {
|
|
475
|
+
requestNotes({ query: query }, function (data) { setNotes(data.notes); setSelected(null) })
|
|
476
|
+
},
|
|
477
|
+
onSelect: function (id, version) {
|
|
478
|
+
requestNotes({ id: id, version: version }, setSelected)
|
|
479
|
+
},
|
|
385
480
|
}),
|
|
386
481
|
h(Footer, null),
|
|
387
482
|
),
|
|
@@ -417,11 +512,6 @@ window.__ModuleLoader__.load({
|
|
|
417
512
|
props.stale ? h('div', { className: 'gr-err' }, T('s0.stale', { path: props.stale })) : null,
|
|
418
513
|
h('div', { className: 'gr-h2' }, T('s0.title')),
|
|
419
514
|
h('div', { className: 'gr-body' }, T('s0.body')),
|
|
420
|
-
h('div', { className: 'gr-steps' },
|
|
421
|
-
h('div', { className: 'gr-step' }, h('b', null, '1'), T('s0.step1')),
|
|
422
|
-
h('div', { className: 'gr-step' }, h('b', null, '2'), T('s0.step2')),
|
|
423
|
-
h('div', { className: 'gr-step' }, h('b', null, '3'), T('s0.step3')),
|
|
424
|
-
),
|
|
425
515
|
props.manual
|
|
426
516
|
? h('div', { className: 'gr-row' },
|
|
427
517
|
h('input', {
|
|
@@ -440,82 +530,594 @@ window.__ModuleLoader__.load({
|
|
|
440
530
|
? h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', disabled: props.busy !== '', onClick: props.onCancel }, T('s0.cancel'))
|
|
441
531
|
: null,
|
|
442
532
|
),
|
|
443
|
-
h('div', { className: 'gr-meta' }, T('s0.hint')),
|
|
444
533
|
)
|
|
445
534
|
}
|
|
446
535
|
|
|
447
|
-
function
|
|
448
|
-
var
|
|
449
|
-
if (props.note) {
|
|
450
|
-
return h('div', { className: 'gr-card' },
|
|
451
|
-
h('div', { className: 'gr-h2 gr-ok' }, '✓ ' + T('s1.saved')),
|
|
452
|
-
h('div', { className: 'gr-body' }, T('s1.savedBody')),
|
|
453
|
-
h('div', { className: 'gr-row' }, h('span', { className: 'gr-path' }, props.note.path)),
|
|
454
|
-
h('div', { className: 'gr-row' }, h('button', { className: 'gr-btn gr-btn-sm', onClick: props.onOverview }, T('s1.view'))),
|
|
455
|
-
)
|
|
456
|
-
}
|
|
536
|
+
function InspirationHome(props) {
|
|
537
|
+
var selected = props.selected
|
|
457
538
|
return h(React.Fragment, null,
|
|
458
539
|
h('div', { className: 'gr-card' },
|
|
459
|
-
h('
|
|
460
|
-
h('div', { className: 'gr-h2' }, T('s1.title')),
|
|
461
|
-
h('div', { className: 'gr-body' }, T('s1.body')),
|
|
540
|
+
h('label', { className: 'gr-h2', htmlFor: 'gr-idea' }, T('idea.title')),
|
|
462
541
|
h('textarea', {
|
|
463
|
-
className: 'gr-textarea', value: props.draft,
|
|
464
|
-
|
|
542
|
+
id: 'gr-idea', className: 'gr-textarea', value: props.draft,
|
|
543
|
+
placeholder: T('idea.placeholder'),
|
|
465
544
|
onChange: function (event) { props.onDraft(event.target.value) },
|
|
466
545
|
onKeyDown: function (event) {
|
|
467
546
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') props.onSave()
|
|
468
547
|
},
|
|
469
548
|
}),
|
|
470
549
|
h('div', { className: 'gr-row' },
|
|
471
|
-
h('button', { className: 'gr-btn', disabled: props.busy
|
|
472
|
-
|
|
473
|
-
h('span', { className: 'gr-meta' }, '⌘↵'),
|
|
550
|
+
h('button', { className: 'gr-btn', disabled: !!props.busy || !props.draft.trim(), onClick: props.onSave }, T('idea.save')),
|
|
551
|
+
h('span', { role: 'status' }, props.saved ? T('idea.saved') : ''),
|
|
474
552
|
),
|
|
475
553
|
),
|
|
476
|
-
h('div', { className: 'gr-
|
|
477
|
-
h('
|
|
478
|
-
h('
|
|
479
|
-
h('button', { className: 'gr-btn gr-btn-ghost
|
|
554
|
+
h('div', { className: 'gr-row' },
|
|
555
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onResearch }, T('research.title')),
|
|
556
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onKnowledge }, T('asset.knowledge')),
|
|
557
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onWorks }, T('asset.works'))),
|
|
558
|
+
h('details', { className: 'gr-card' },
|
|
559
|
+
h('summary', null, T('idea.find')),
|
|
560
|
+
h('form', { className: 'gr-row', onSubmit: function (event) {
|
|
561
|
+
event.preventDefault()
|
|
562
|
+
props.onSearch()
|
|
563
|
+
} },
|
|
564
|
+
h('input', { className: 'gr-input', type: 'search', 'aria-label': T('idea.find'),
|
|
565
|
+
value: props.query, onChange: function (event) { props.onQuery(event.target.value) } }),
|
|
566
|
+
h('button', { className: 'gr-btn gr-btn-sm', disabled: !!props.busy }, T('idea.search'))),
|
|
567
|
+
props.notes && props.notes.length === 0 ? h('p', null, T('idea.empty')) : null,
|
|
568
|
+
(props.notes || []).map(function (note) {
|
|
569
|
+
return h('div', { key: note.id },
|
|
570
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy,
|
|
571
|
+
onClick: function () { props.onSelect(note.id, note.version) } }, note.title))
|
|
572
|
+
}),
|
|
573
|
+
selected ? h('article', { className: 'gr-card' },
|
|
574
|
+
h('div', { className: 'gr-h2' }, selected.meta.title),
|
|
575
|
+
h('pre', { className: 'gr-code' }, selected.body),
|
|
576
|
+
h('label', null, T('idea.version'), ' ',
|
|
577
|
+
h('select', { value: selected.meta.version, disabled: !!props.busy,
|
|
578
|
+
onChange: function (event) { props.onSelect(selected.meta.id, Number(event.target.value)) } },
|
|
579
|
+
selected.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))),
|
|
580
|
+
) : null,
|
|
480
581
|
),
|
|
481
582
|
)
|
|
482
583
|
}
|
|
483
584
|
|
|
484
|
-
function
|
|
485
|
-
var
|
|
486
|
-
var
|
|
487
|
-
var
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
|
|
585
|
+
function ModelSettings() {
|
|
586
|
+
var [config, setConfig] = useState(null)
|
|
587
|
+
var [busy, setBusy] = useState(false)
|
|
588
|
+
var [message, setMessage] = useState('')
|
|
589
|
+
var [error, setError] = useState(null)
|
|
590
|
+
|
|
591
|
+
function change(key, value) {
|
|
592
|
+
setConfig(function (current) { return Object.assign({}, current, { [key]: value }) })
|
|
593
|
+
setMessage('')
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function submit(event) {
|
|
597
|
+
event.preventDefault()
|
|
598
|
+
var test = event.nativeEvent.submitter?.value === 'test'
|
|
599
|
+
setBusy(true)
|
|
600
|
+
setError(null)
|
|
601
|
+
setMessage('')
|
|
602
|
+
try {
|
|
603
|
+
await api(MODEL, Object.assign({}, config, { test: test }))
|
|
604
|
+
if (test) { setMessage(T('model.tested')); return }
|
|
605
|
+
for (var attempt = 0; attempt < 12; attempt++) {
|
|
606
|
+
await new Promise(function (resolve) { setTimeout(resolve, 400) })
|
|
607
|
+
var current = await api(MODEL).catch(function () { return null })
|
|
608
|
+
if (current && current.enabled === config.enabled &&
|
|
609
|
+
(!current.enabled || (current.name === config.name && current.endpoint === config.endpoint))) {
|
|
610
|
+
setMessage(T('research.saved'))
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
throw new Error(T('parsers.failed'))
|
|
615
|
+
} catch (problem) { setError(problem) }
|
|
616
|
+
finally { setBusy(false) }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return h('details', { className: 'gr-card', onToggle: function (event) {
|
|
620
|
+
if (event.currentTarget.open && !config) api(MODEL).then(setConfig, setError)
|
|
621
|
+
} },
|
|
622
|
+
h('summary', null, T('model.title')),
|
|
623
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
624
|
+
config ? h('form', { onSubmit: submit },
|
|
625
|
+
h('label', null, h('input', { type: 'checkbox', checked: config.enabled, disabled: busy,
|
|
626
|
+
onChange: function (event) { change('enabled', event.target.checked) } }), T('model.enabled')),
|
|
627
|
+
config.enabled ? h(React.Fragment, null,
|
|
628
|
+
h('label', null, T('model.endpoint'), h('input', { type: 'url', className: 'gr-input', value: config.endpoint || '',
|
|
629
|
+
required: true, disabled: busy, onChange: function (event) { change('endpoint', event.target.value) } })),
|
|
630
|
+
h('label', null, T('model.name'), h('input', { className: 'gr-input', value: config.name || '',
|
|
631
|
+
required: true, disabled: busy, onChange: function (event) { change('name', event.target.value) } })),
|
|
632
|
+
h('p', { className: 'gr-meta' }, 'GROVE_MODEL_KEY')) : null,
|
|
633
|
+
h('div', { className: 'gr-row' },
|
|
634
|
+
config.enabled ? h('button', { className: 'gr-btn', value: 'test', disabled: busy }, T('model.test')) : null,
|
|
635
|
+
h('button', { className: 'gr-btn', value: 'apply', disabled: busy }, T('model.apply'))),
|
|
636
|
+
h('p', { role: 'status' }, message)) : null)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function ResearchFindings(props) {
|
|
640
|
+
var [question, setQuestion] = useState(props.title)
|
|
641
|
+
var [findings, setFindings] = useState([])
|
|
642
|
+
var [evidence, setEvidence] = useState([])
|
|
643
|
+
var [selected, setSelected] = useState([])
|
|
644
|
+
var [busy, setBusy] = useState(false)
|
|
645
|
+
var [error, setError] = useState(null)
|
|
646
|
+
|
|
647
|
+
function load(event) {
|
|
648
|
+
if (!event.currentTarget.open) return
|
|
649
|
+
api(FINDINGS, { action: 'list', id: props.id }).then(function (result) { setFindings(result.findings) }, setError)
|
|
650
|
+
api(REPORTS, { action: 'list', research: props.id }).then(function (result) { setEvidence(result.evidence) }, setError)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function ask(event) {
|
|
654
|
+
event.preventDefault()
|
|
655
|
+
setBusy(true)
|
|
656
|
+
setError(null)
|
|
657
|
+
try {
|
|
658
|
+
var finding = await api(FINDINGS, { id: props.id, version: props.version, question: question,
|
|
659
|
+
includeHistory: true, materials: evidence.filter(function (item) { return selected.includes(item.id) }).map(function (item) {
|
|
660
|
+
return { id: item.id, version: item.version }
|
|
661
|
+
}) })
|
|
662
|
+
setFindings(function (items) { return items.concat(finding) })
|
|
663
|
+
setQuestion('')
|
|
664
|
+
} catch (problem) { setError(problem) }
|
|
665
|
+
finally { setBusy(false) }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return h('details', { className: 'gr-card', onToggle: load },
|
|
669
|
+
h('summary', null, T('model.ask')),
|
|
670
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
671
|
+
findings.map(function (item) {
|
|
672
|
+
return h('article', { key: item.meta.id }, h('h3', null, item.meta.title), h('pre', { className: 'gr-code' }, item.body),
|
|
673
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
674
|
+
api(REPORTS, { action: 'draft', research: props.id, id: item.meta.id, version: item.meta.version }).then(props.onDraft, setError)
|
|
675
|
+
} }, T('report.review')))
|
|
676
|
+
}),
|
|
677
|
+
h('form', { onSubmit: ask },
|
|
678
|
+
h('label', null, T('model.question'), h('textarea', { className: 'gr-textarea', value: question,
|
|
679
|
+
required: true, maxLength: 1000, onChange: function (event) { setQuestion(event.target.value) } })),
|
|
680
|
+
h('p', { className: 'gr-meta' }, T('model.scope')),
|
|
681
|
+
evidence.map(function (item) {
|
|
682
|
+
return h('label', { key: item.id }, h('input', { type: 'checkbox', checked: selected.includes(item.id),
|
|
683
|
+
onChange: function (event) { setSelected(event.target.checked ? selected.concat(item.id) : selected.filter(function (id) { return id !== item.id })) } }), item.title)
|
|
684
|
+
}),
|
|
685
|
+
h('button', { className: 'gr-btn', disabled: busy }, T('model.ask'))))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function ParserSettings() {
|
|
689
|
+
var [enabled, setEnabled] = useState(null)
|
|
690
|
+
var [pending, setPending] = useState(false)
|
|
691
|
+
var [done, setDone] = useState(false)
|
|
692
|
+
var [error, setError] = useState(null)
|
|
693
|
+
|
|
694
|
+
async function apply(event) {
|
|
695
|
+
event.preventDefault()
|
|
696
|
+
setPending(true)
|
|
697
|
+
setDone(false)
|
|
698
|
+
setError(null)
|
|
699
|
+
try {
|
|
700
|
+
await api(PARSERS, { pdf: enabled })
|
|
701
|
+
for (var attempt = 0; attempt < 12; attempt++) {
|
|
702
|
+
await new Promise(function (resolve) { setTimeout(resolve, 400) })
|
|
703
|
+
var current = await api(PARSERS).catch(function () { return null })
|
|
704
|
+
if (current && current.pdf === enabled) {
|
|
705
|
+
setDone(true)
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
throw new Error(T('parsers.failed'))
|
|
710
|
+
} catch (problem) {
|
|
711
|
+
setError(problem)
|
|
712
|
+
} finally {
|
|
713
|
+
setPending(false)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return h('details', { className: 'gr-card', onToggle: function (event) {
|
|
718
|
+
if (!event.currentTarget.open || enabled !== null) return
|
|
719
|
+
api(PARSERS).then(function (current) { setEnabled(current.pdf); setError(null) }, setError)
|
|
720
|
+
} },
|
|
721
|
+
h('summary', null, T('parsers.title')),
|
|
722
|
+
h('p', { className: 'gr-body' }, T('parsers.hint')),
|
|
723
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
724
|
+
enabled === null ? null : h('form', { onSubmit: apply },
|
|
725
|
+
h('label', null, h('input', { type: 'checkbox', checked: enabled, disabled: pending,
|
|
726
|
+
onChange: function (event) { setEnabled(event.target.checked); setDone(false) } }), T('parsers.pdf')),
|
|
727
|
+
h('button', { className: 'gr-btn', disabled: pending }, T(pending ? 'parsers.pending' : 'parsers.apply')),
|
|
728
|
+
h('p', { role: 'status' }, done ? T('parsers.done') : '')))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function readSourceFile(file) {
|
|
732
|
+
if (file.size > 500000) throw new Error(T('err.too-large'))
|
|
733
|
+
var bytes = new Uint8Array(await file.arrayBuffer())
|
|
734
|
+
var binary = ''
|
|
735
|
+
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
|
|
736
|
+
return btoa(binary)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function ResearchSources(props) {
|
|
740
|
+
var [items, setItems] = useState([])
|
|
741
|
+
var [document, setDocument] = useState(null)
|
|
742
|
+
var [busy, setBusy] = useState(false)
|
|
743
|
+
var [error, setError] = useState(null)
|
|
744
|
+
var [saved, setSaved] = useState(false)
|
|
745
|
+
useEffect(function () {
|
|
746
|
+
var active = true
|
|
747
|
+
api(SOURCES, { action: 'list', research: props.id }).then(function (value) {
|
|
748
|
+
if (active) setItems(value.sources)
|
|
749
|
+
}, function (problem) {
|
|
750
|
+
if (active) setError(problem)
|
|
751
|
+
})
|
|
752
|
+
return function () { active = false }
|
|
753
|
+
}, [props.id])
|
|
754
|
+
|
|
755
|
+
async function run(operation) {
|
|
756
|
+
setBusy(true)
|
|
757
|
+
setError(null)
|
|
758
|
+
setSaved(false)
|
|
759
|
+
try { await operation() }
|
|
760
|
+
catch (problem) { setError(problem) }
|
|
761
|
+
finally { setBusy(false) }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function upload(event) {
|
|
765
|
+
var file = event.target.files[0]
|
|
766
|
+
event.target.value = ''
|
|
767
|
+
if (!file) return
|
|
768
|
+
await run(async function () {
|
|
769
|
+
var original = await readSourceFile(file)
|
|
770
|
+
var result = await api(SOURCES, { action: 'import', name: file.name, original: original, research: props.id })
|
|
771
|
+
setItems(function (existing) { return [result.meta].concat(existing.filter(function (item) { return item.id !== result.meta.id })) })
|
|
772
|
+
setDocument({ meta: result.meta, blocks: result.blocks, original: original })
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function download() {
|
|
777
|
+
var bytes = Uint8Array.from(atob(document.original), function (character) { return character.charCodeAt(0) })
|
|
778
|
+
var url = URL.createObjectURL(new Blob([bytes]))
|
|
779
|
+
var link = window.document.createElement('a')
|
|
780
|
+
link.href = url
|
|
781
|
+
link.download = document.meta.title
|
|
782
|
+
link.click()
|
|
783
|
+
setTimeout(function () { URL.revokeObjectURL(url) }, 1000)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return h('section', { className: 'gr-card' },
|
|
787
|
+
h('h2', { className: 'gr-h2' }, T('source.title')),
|
|
788
|
+
h('label', null, T('source.import'), ' ',
|
|
789
|
+
h('input', { type: 'file', accept: '.md,.markdown,.pdf', disabled: busy, onChange: upload })),
|
|
790
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
791
|
+
h('p', { role: 'status' }, saved ? T('source.saved') : ''),
|
|
792
|
+
items.map(function (item) {
|
|
793
|
+
return h('button', { key: item.id, className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
794
|
+
run(async function () { setDocument(await api(SOURCES, { action: 'open', id: item.id, version: item.version })) })
|
|
795
|
+
} }, item.title)
|
|
796
|
+
}),
|
|
797
|
+
document ? h('div', null,
|
|
798
|
+
h('button', { className: 'gr-btn', onClick: download }, T('source.open')),
|
|
799
|
+
document.blocks.map(function (block, index) {
|
|
800
|
+
return h('details', { key: document.meta.id + ':' + index },
|
|
801
|
+
h('summary', null, T('source.read'), ' · ', block.locator),
|
|
802
|
+
h('pre', { className: 'gr-code' }, block.text),
|
|
803
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: function () {
|
|
804
|
+
run(async function () {
|
|
805
|
+
await api(SOURCES, { action: 'excerpt', id: document.meta.id, version: document.meta.version, index: index })
|
|
806
|
+
setSaved(true)
|
|
807
|
+
})
|
|
808
|
+
} }, T('source.save')))
|
|
809
|
+
})) : null)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function ResearchReports(props) {
|
|
813
|
+
var [catalog, setCatalog] = useState(null)
|
|
814
|
+
var [report, setReport] = useState(null)
|
|
815
|
+
var [body, setBody] = useState('')
|
|
816
|
+
var [references, setReferences] = useState([])
|
|
817
|
+
var [originals, setOriginals] = useState({})
|
|
818
|
+
var [busy, setBusy] = useState(false)
|
|
819
|
+
var [error, setError] = useState(null)
|
|
820
|
+
var [editing, setEditing] = useState(false)
|
|
821
|
+
|
|
822
|
+
useEffect(function () {
|
|
823
|
+
if (!props.draft) return
|
|
824
|
+
setBody(props.draft.body)
|
|
825
|
+
setReferences(props.draft.evidence)
|
|
826
|
+
setReport(null)
|
|
827
|
+
setEditing(true)
|
|
828
|
+
list().catch(setError)
|
|
829
|
+
}, [props.draft])
|
|
830
|
+
|
|
831
|
+
function list() {
|
|
832
|
+
return api(REPORTS, { action: 'list', research: props.id }).then(setCatalog)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function run(operation) {
|
|
836
|
+
setBusy(true)
|
|
837
|
+
setError(null)
|
|
838
|
+
try { await operation() }
|
|
839
|
+
catch (problem) { setError(problem) }
|
|
840
|
+
finally { setBusy(false) }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function read(id, version) {
|
|
844
|
+
var result = await api(REPORTS, { action: 'read', id: id, version: version })
|
|
845
|
+
setReport(result)
|
|
846
|
+
setBody(result.body)
|
|
847
|
+
setReferences(result.evidence.filter(function (item) { return !item.missing }).map(function (item) {
|
|
848
|
+
return { id: item.meta.id, version: item.meta.version }
|
|
849
|
+
}))
|
|
850
|
+
setOriginals({})
|
|
851
|
+
setEditing(true)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function start() {
|
|
855
|
+
setReport(null)
|
|
856
|
+
setBody('')
|
|
857
|
+
setReferences([])
|
|
858
|
+
setEditing(true)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function save(event) {
|
|
862
|
+
event.preventDefault()
|
|
863
|
+
run(async function () {
|
|
864
|
+
var input = { action: 'confirm', research: props.id, body: body, evidence: references }
|
|
865
|
+
if (report) { input.id = report.meta.id; input.version = report.meta.version }
|
|
866
|
+
var result = await api(REPORTS, input)
|
|
867
|
+
await read(result.meta.id, result.meta.version)
|
|
868
|
+
await list()
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function cite(item) {
|
|
873
|
+
if (item.missing) return h('p', { key: item.reference.id }, T('report.missingEvidence'))
|
|
874
|
+
var ref = item.original
|
|
875
|
+
return h('div', { key: item.meta.id, className: 'gr-card' },
|
|
876
|
+
h('p', null, item.meta.source, ' · ', item.meta.locator),
|
|
877
|
+
h('pre', { className: 'gr-code' }, item.body),
|
|
878
|
+
ref ? h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
879
|
+
run(async function () {
|
|
880
|
+
var text
|
|
881
|
+
try {
|
|
882
|
+
var result = await api(SOURCES, { action: 'open', id: ref.id, version: ref.version })
|
|
883
|
+
var block = result.blocks.find(function (entry) { return entry.locator === item.meta.locator })
|
|
884
|
+
text = block ? block.text : T('report.missing')
|
|
885
|
+
} catch { text = T('report.missing') }
|
|
886
|
+
setOriginals(function (previous) { return Object.assign({}, previous, { [item.meta.id]: text }) })
|
|
887
|
+
})
|
|
888
|
+
} }, T('source.read')) : null,
|
|
889
|
+
originals[item.meta.id] ? h('pre', { className: 'gr-code' }, originals[item.meta.id]) : null)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return h('details', { className: 'gr-card', open: props.draft ? true : undefined, onToggle: function (event) {
|
|
893
|
+
if (event.currentTarget.open) run(list)
|
|
894
|
+
} },
|
|
895
|
+
h('summary', null, T('report.title')),
|
|
896
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
897
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: start }, T('report.new')),
|
|
898
|
+
(catalog ? catalog.reports : []).map(function (entry) {
|
|
899
|
+
return h('button', { key: entry.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
900
|
+
onClick: function () { run(function () { return read(entry.meta.id, entry.meta.version) }) } }, entry.meta.title)
|
|
901
|
+
}),
|
|
902
|
+
report ? h('label', null, T('report.history'), ' ', h('select', {
|
|
903
|
+
value: report.meta.version, disabled: busy,
|
|
904
|
+
onChange: function (event) {
|
|
905
|
+
var version = Number(event.target.value)
|
|
906
|
+
run(function () { return read(report.meta.id, version) })
|
|
907
|
+
},
|
|
908
|
+
}, report.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))) : null,
|
|
909
|
+
report ? report.evidence.map(cite) : null,
|
|
910
|
+
report ? h('div', { className: 'gr-row' },
|
|
911
|
+
h('button', { className: 'gr-btn', onClick: function () {
|
|
912
|
+
props.onNavigate('knowledge', { title: report.meta.title, body: report.body, report: { id: report.meta.id, version: report.meta.version } })
|
|
913
|
+
} }, T('asset.derive')),
|
|
914
|
+
h('button', { className: 'gr-btn', onClick: function () {
|
|
915
|
+
props.onNavigate('works', { title: report.meta.title, body: report.body, basis: { id: report.meta.id, version: report.meta.version } })
|
|
916
|
+
} }, T('asset.compose'))) : null,
|
|
917
|
+
editing ? h('form', { onSubmit: save },
|
|
918
|
+
h('label', null, T('report.body'), h('textarea', { className: 'gr-textarea', required: true,
|
|
919
|
+
value: body, onChange: function (event) { setBody(event.target.value) } })),
|
|
920
|
+
(catalog ? catalog.evidence : []).map(function (item) {
|
|
921
|
+
var checked = references.some(function (ref) { return ref.id === item.id && ref.version === item.version })
|
|
922
|
+
return h('label', { key: item.id }, h('input', { type: 'checkbox', checked: checked,
|
|
923
|
+
onChange: function () {
|
|
924
|
+
setReferences(checked ? references.filter(function (ref) { return ref.id !== item.id })
|
|
925
|
+
: references.filter(function (ref) { return ref.id !== item.id }).concat({ id: item.id, version: item.version }))
|
|
926
|
+
} }), item.title, ' · v', item.version)
|
|
927
|
+
}),
|
|
928
|
+
h('button', { className: 'gr-btn', disabled: busy || (report && report.meta.version !== report.versions[0]) }, T('report.save')),
|
|
929
|
+
h('button', { type: 'button', className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
930
|
+
onClick: function () { setEditing(false); props.onDiscard() } }, T('report.discard'))) : null)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function AssetPage(props) {
|
|
934
|
+
var isKnowledge = props.kind === 'knowledge'
|
|
935
|
+
var endpoint = isKnowledge ? KNOWLEDGE : WORKS
|
|
936
|
+
var [items, setItems] = useState([])
|
|
937
|
+
var [selected, setSelected] = useState(null)
|
|
938
|
+
var [draft, setDraft] = useState(props.seed)
|
|
939
|
+
var [title, setTitle] = useState(props.seed?.title || '')
|
|
940
|
+
var [body, setBody] = useState(props.seed?.body || '')
|
|
941
|
+
var [kind, setKind] = useState('concept')
|
|
942
|
+
var [reason, setReason] = useState('')
|
|
943
|
+
var [basis, setBasis] = useState(null)
|
|
944
|
+
var [busy, setBusy] = useState(false)
|
|
945
|
+
var [error, setError] = useState(null)
|
|
946
|
+
var [message, setMessage] = useState('')
|
|
947
|
+
|
|
948
|
+
function list() { return api(endpoint, { action: 'list' }).then(function (result) { setItems(result.items) }) }
|
|
949
|
+
useEffect(function () { list().catch(setError) }, [endpoint])
|
|
950
|
+
|
|
951
|
+
async function run(operation) {
|
|
952
|
+
setBusy(true)
|
|
953
|
+
setError(null)
|
|
954
|
+
setMessage('')
|
|
955
|
+
try { await operation() }
|
|
956
|
+
catch (problem) { setError(problem) }
|
|
957
|
+
finally { setBusy(false) }
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function read(id, version) {
|
|
961
|
+
var entry = await api(endpoint, { action: 'read', id: id, version: version })
|
|
962
|
+
setSelected(entry)
|
|
963
|
+
setDraft({})
|
|
964
|
+
setTitle(entry.meta.title)
|
|
965
|
+
setBody(entry.body)
|
|
966
|
+
setBasis(null)
|
|
967
|
+
setReason('')
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function start() {
|
|
971
|
+
setSelected(null)
|
|
972
|
+
setDraft({})
|
|
973
|
+
setTitle('')
|
|
974
|
+
setBody('')
|
|
975
|
+
setBasis(null)
|
|
976
|
+
setReason('')
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function save(event) {
|
|
980
|
+
event.preventDefault()
|
|
981
|
+
run(async function () {
|
|
982
|
+
var input = { action: 'save', body: body, title: title, type: kind, reason: reason,
|
|
983
|
+
basis: draft?.basis, report: draft?.report }
|
|
984
|
+
if (selected) { input.id = selected.meta.id; input.version = selected.meta.version }
|
|
985
|
+
var entry = await api(endpoint, input)
|
|
986
|
+
await read(entry.meta.id, entry.meta.version)
|
|
987
|
+
await list()
|
|
988
|
+
setMessage(T('research.saved'))
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function readBasis() {
|
|
993
|
+
run(async function () {
|
|
994
|
+
var ref = selected.meta.links.find(function (link) { return link.kind === 'derived' })
|
|
995
|
+
var input = { action: 'read', id: ref.id, version: ref.version }
|
|
996
|
+
var entry = await api(isKnowledge ? REPORTS : KNOWLEDGE, input)
|
|
997
|
+
.catch(function () { return isKnowledge ? null : api(REPORTS, input).catch(function () { return null }) })
|
|
998
|
+
setBasis(entry || { body: T('asset.missing') })
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function reference() { return { id: selected.meta.id, version: selected.meta.version } }
|
|
1003
|
+
|
|
494
1004
|
return h(React.Fragment, null,
|
|
495
|
-
h('div', { className: 'gr-
|
|
496
|
-
h('
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
h('
|
|
517
|
-
|
|
518
|
-
|
|
1005
|
+
h('div', { className: 'gr-row' },
|
|
1006
|
+
h('button', { className: 'gr-btn gr-btn-ghost', onClick: function () { props.onNavigate('home') } }, T('research.back')),
|
|
1007
|
+
h('h2', { className: 'gr-h2' }, T('asset.' + props.kind)),
|
|
1008
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: start }, T('asset.new'))),
|
|
1009
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
1010
|
+
h('p', { role: 'status' }, message),
|
|
1011
|
+
items.map(function (entry) {
|
|
1012
|
+
return h('button', { key: entry.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
1013
|
+
onClick: function () { run(function () { return read(entry.meta.id, entry.meta.version) }) } }, entry.meta.title)
|
|
1014
|
+
}),
|
|
1015
|
+
draft ? h('form', { className: 'gr-card', onSubmit: save },
|
|
1016
|
+
selected ? h('h3', null, selected.meta.title) : h('label', null, T('asset.title'),
|
|
1017
|
+
h('input', { className: 'gr-input', value: title, maxLength: 300, required: true, onChange: function (event) { setTitle(event.target.value) } })),
|
|
1018
|
+
isKnowledge && !selected ? h('label', null, T('asset.type'),
|
|
1019
|
+
h('select', { value: kind, onChange: function (event) { setKind(event.target.value) } },
|
|
1020
|
+
['concept', 'theory', 'claim', 'method'].map(function (type) { return h('option', { key: type, value: type }, T('type.' + type)) }))) : null,
|
|
1021
|
+
selected ? h('label', null, T('report.history'),
|
|
1022
|
+
h('select', { value: selected.meta.version, disabled: busy, onChange: function (event) {
|
|
1023
|
+
var version = Number(event.target.value)
|
|
1024
|
+
run(function () { return read(selected.meta.id, version) })
|
|
1025
|
+
} }, selected.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))) : null,
|
|
1026
|
+
h('label', null, T('asset.body'), h('textarea', { className: 'gr-textarea', value: body, required: true,
|
|
1027
|
+
onChange: function (event) { setBody(event.target.value) } })),
|
|
1028
|
+
selected?.meta.revisionReason ? h('p', { className: 'gr-meta' }, selected.meta.revisionReason) : null,
|
|
1029
|
+
isKnowledge && selected ? h('label', null, T('asset.reason'),
|
|
1030
|
+
h('textarea', { className: 'gr-textarea', value: reason, required: true, onChange: function (event) { setReason(event.target.value) } })) : null,
|
|
1031
|
+
h('button', { className: 'gr-btn', disabled: busy || (selected && selected.meta.version !== selected.versions[0]) }, T('asset.save'))) : null,
|
|
1032
|
+
selected ? h('div', { className: 'gr-row' },
|
|
1033
|
+
selected.meta.links?.some(function (link) { return link.kind === 'derived' })
|
|
1034
|
+
? h('button', { className: 'gr-btn', disabled: busy, onClick: readBasis }, T('asset.basis')) : null,
|
|
1035
|
+
isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1036
|
+
props.onNavigate('research', { title: selected.meta.title, origin: reference() })
|
|
1037
|
+
} }, T('asset.research')) : null,
|
|
1038
|
+
isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1039
|
+
props.onNavigate('works', { title: selected.meta.title, body: selected.body, basis: reference() })
|
|
1040
|
+
} }, T('asset.compose')) : null,
|
|
1041
|
+
!isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1042
|
+
run(async function () { await navigator.clipboard.writeText(selected.body); setMessage(T('asset.copied')) })
|
|
1043
|
+
} }, T('asset.copy')) : null) : null,
|
|
1044
|
+
basis ? h('article', { className: 'gr-card' },
|
|
1045
|
+
basis.meta ? h('h3', null, basis.meta.title, ' · v', basis.meta.version) : null,
|
|
1046
|
+
h('pre', { className: 'gr-code' }, basis.body)) : null)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function ResearchPage(props) {
|
|
1050
|
+
var [reportDraft, setReportDraft] = useState(null)
|
|
1051
|
+
var [studies, setStudies] = useState([])
|
|
1052
|
+
var [selected, setSelected] = useState(null)
|
|
1053
|
+
var [question, setQuestion] = useState(props.seed?.title || '')
|
|
1054
|
+
var [body, setBody] = useState('')
|
|
1055
|
+
var [busy, setBusy] = useState(true)
|
|
1056
|
+
var [error, setError] = useState(null)
|
|
1057
|
+
var [saved, setSaved] = useState(false)
|
|
1058
|
+
|
|
1059
|
+
useEffect(function () {
|
|
1060
|
+
var active = true
|
|
1061
|
+
api(RESEARCH).then(function (data) {
|
|
1062
|
+
if (active) setStudies(data.studies)
|
|
1063
|
+
}, function (problem) {
|
|
1064
|
+
if (active) setError(problem)
|
|
1065
|
+
}).finally(function () {
|
|
1066
|
+
if (active) setBusy(false)
|
|
1067
|
+
})
|
|
1068
|
+
return function () { active = false }
|
|
1069
|
+
}, [])
|
|
1070
|
+
|
|
1071
|
+
function select(note) {
|
|
1072
|
+
setSelected(note)
|
|
1073
|
+
setReportDraft(null)
|
|
1074
|
+
setBody(note ? note.body : '')
|
|
1075
|
+
setQuestion('')
|
|
1076
|
+
setSaved(false)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function save(event) {
|
|
1080
|
+
event.preventDefault()
|
|
1081
|
+
if (busy || !body.trim() || (!selected && !question.trim())) return
|
|
1082
|
+
setBusy(true)
|
|
1083
|
+
setError(null)
|
|
1084
|
+
setSaved(false)
|
|
1085
|
+
var input = selected ? { id: selected.meta.id, version: selected.meta.version, body: body } : { question: question, body: body, origin: props.seed?.origin }
|
|
1086
|
+
api(RESEARCH, input).then(function (note) {
|
|
1087
|
+
select(note)
|
|
1088
|
+
setSaved(true)
|
|
1089
|
+
setStudies(function (items) {
|
|
1090
|
+
return [note].concat(items.filter(function (item) { return item.meta.id !== note.meta.id }))
|
|
1091
|
+
})
|
|
1092
|
+
}, setError).finally(function () { setBusy(false) })
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return h(React.Fragment, null,
|
|
1096
|
+
h('div', { className: 'gr-row' },
|
|
1097
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: props.onBack }, T('research.back')),
|
|
1098
|
+
studies.length ? h('button', { className: 'gr-btn', disabled: busy, onClick: function () { select(studies[0]) } }, T('research.continue')) : null,
|
|
1099
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () { select(null) } }, T('research.new'))),
|
|
1100
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
1101
|
+
h('form', { className: 'gr-card', onSubmit: save },
|
|
1102
|
+
selected ? h('h2', { className: 'gr-h2' }, selected.meta.title) :
|
|
1103
|
+
h('label', null, T('research.question'),
|
|
1104
|
+
h('input', { className: 'gr-input', value: question, required: true, maxLength: 300,
|
|
1105
|
+
onChange: function (event) { setQuestion(event.target.value) } })),
|
|
1106
|
+
h('label', null, T('research.notes'),
|
|
1107
|
+
h('textarea', { className: 'gr-textarea', value: body, required: true,
|
|
1108
|
+
onChange: function (event) { setBody(event.target.value); setSaved(false) } })),
|
|
1109
|
+
h('div', { className: 'gr-row' },
|
|
1110
|
+
h('button', { className: 'gr-btn', disabled: busy }, T('research.save')),
|
|
1111
|
+
h('span', { role: 'status' }, saved ? T('research.saved') : ''))),
|
|
1112
|
+
h(ModelSettings),
|
|
1113
|
+
h(ParserSettings),
|
|
1114
|
+
selected ? h(ResearchFindings, { key: 'findings:' + selected.meta.id, id: selected.meta.id, title: selected.meta.title, version: selected.meta.version, onDraft: setReportDraft }) : null,
|
|
1115
|
+
selected ? h(ResearchSources, { key: selected.meta.id, id: selected.meta.id }) : null,
|
|
1116
|
+
selected ? h(ResearchReports, { key: 'report:' + selected.meta.id, id: selected.meta.id, onNavigate: props.onNavigate, draft: reportDraft, onDiscard: function () { setReportDraft(null) } }) : null,
|
|
1117
|
+
studies.map(function (note) {
|
|
1118
|
+
return h('button', { key: note.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
1119
|
+
onClick: function () { select(note) } }, note.meta.title)
|
|
1120
|
+
}),
|
|
519
1121
|
)
|
|
520
1122
|
}
|
|
521
1123
|
|
|
@@ -529,8 +1131,6 @@ window.__ModuleLoader__.load({
|
|
|
529
1131
|
function Footer() {
|
|
530
1132
|
return h('div', { className: 'gr-foot' },
|
|
531
1133
|
h('div', null, T('foot.privacy')),
|
|
532
|
-
h('div', null, T('foot.confirm')),
|
|
533
|
-
h('div', null, T('foot.data')),
|
|
534
1134
|
)
|
|
535
1135
|
}
|
|
536
1136
|
|
|
@@ -582,9 +1182,6 @@ window.__ModuleLoader__.load({
|
|
|
582
1182
|
'.gr-err{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap}',
|
|
583
1183
|
'.gr-ok{color:var(--dsw-alias-state-success-primary)}',
|
|
584
1184
|
'.gr-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:2px 6px;word-break:break-all}',
|
|
585
|
-
'.gr-steps{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}',
|
|
586
|
-
'.gr-step{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px;font-size:12px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base)}',
|
|
587
|
-
'.gr-step b{display:block;color:var(--dsw-alias-label-primary);font-size:12px;margin-bottom:2px}',
|
|
588
1185
|
'.gr-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:10px}',
|
|
589
1186
|
'.gr-stat{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-base);min-width:0}',
|
|
590
1187
|
'.gr-stat b{display:block;font-size:20px;font-weight:600;line-height:1.2}',
|
|
@@ -596,7 +1193,7 @@ window.__ModuleLoader__.load({
|
|
|
596
1193
|
'.gr-itemtitle{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
597
1194
|
'.gr-code{white-space:pre-wrap;word-break:break-word;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;margin:0}',
|
|
598
1195
|
'.gr-foot{border-top:1px solid var(--dsw-alias-border-l1);padding-top:12px;color:var(--dsw-alias-label-secondary);font-size:12px;display:flex;flex-direction:column;gap:4px}',
|
|
599
|
-
'@media (max-width:560px){.gr-inner{padding:18px 14px 32px}.gr-head{flex-wrap:wrap}.gr-
|
|
1196
|
+
'@media (max-width:560px){.gr-inner{padding:18px 14px 32px}.gr-head{flex-wrap:wrap}.gr-stats{grid-template-columns:repeat(2,1fr)}.gr-stat:last-child:nth-child(odd){grid-column:1/-1}.gr-btn{align-self:stretch;justify-content:center}}',
|
|
600
1197
|
'@media (prefers-reduced-motion:reduce){.gr-btn{transition:none}}',
|
|
601
1198
|
].join('\n')
|
|
602
1199
|
|
|
@@ -613,10 +1210,16 @@ window.__ModuleLoader__.load({
|
|
|
613
1210
|
console.warn('[grove] locale registration failed', error)
|
|
614
1211
|
}
|
|
615
1212
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1213
|
+
if (typeof ctx.inject === 'function') {
|
|
1214
|
+
ctx.inject(['remote', 'remote.directoryPicker'], function (scoped) {
|
|
1215
|
+
var picker = scoped.remote.directoryPicker
|
|
1216
|
+
scoped.effect(function () {
|
|
1217
|
+
services.picker = picker
|
|
1218
|
+
return function () {
|
|
1219
|
+
if (services.picker === picker) services.picker = null
|
|
1220
|
+
}
|
|
1221
|
+
}, 'grove: directory picker')
|
|
1222
|
+
})
|
|
620
1223
|
}
|
|
621
1224
|
var slots = ctx.slots
|
|
622
1225
|
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|