dsh-grove 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +34 -74
- package/README.md +35 -77
- package/client.js +760 -143
- package/package.json +3 -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,10 +19,83 @@ 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
|
+
'asset.knowledge': '知识库',
|
|
37
|
+
'asset.works': '作品',
|
|
38
|
+
'asset.title': '标题',
|
|
39
|
+
'asset.body': '正文',
|
|
40
|
+
'asset.new': '新建',
|
|
41
|
+
'asset.save': '保存',
|
|
42
|
+
'asset.basis': '查看依据',
|
|
43
|
+
'asset.derive': '提炼为知识',
|
|
44
|
+
'asset.compose': '写成作品',
|
|
45
|
+
'asset.research': '带着这条知识深挖',
|
|
46
|
+
'asset.reason': '实践反思 · 什么改变了你的理解?',
|
|
47
|
+
'asset.copy': '复制正文',
|
|
48
|
+
'asset.copied': '已复制',
|
|
49
|
+
'asset.type': '知识类型',
|
|
50
|
+
'asset.missing': '来源不可用。',
|
|
51
|
+
'report.review': '审核为报告草稿',
|
|
52
|
+
'report.discard': '丢弃未保存草稿',
|
|
53
|
+
'model.title': '研究模型',
|
|
54
|
+
'model.enabled': '启用 AI 深挖',
|
|
55
|
+
'model.endpoint': 'HTTPS 模型接口',
|
|
56
|
+
'model.name': '模型名称',
|
|
57
|
+
'model.test': '测试连接',
|
|
58
|
+
'model.apply': '应用模型设置',
|
|
59
|
+
'model.tested': '连接通过。',
|
|
60
|
+
'model.ask': '请 AI 深挖',
|
|
61
|
+
'model.question': '想往哪儿深挖?',
|
|
62
|
+
'model.scope': '仅发送勾选的摘录和本研究先前的发现。',
|
|
63
|
+
'parsers.title': '资料解析设置',
|
|
64
|
+
'parsers.pdf': '启用文本 PDF 解析',
|
|
65
|
+
'parsers.apply': '应用设置',
|
|
66
|
+
'parsers.pending': '正在应用…',
|
|
67
|
+
'parsers.done': '解析设置已应用。',
|
|
68
|
+
'parsers.failed': '设置还没生效,请重试。',
|
|
69
|
+
'parsers.hint': 'Markdown 始终可导入。关闭 PDF 解析后,旧证据仍可回看。',
|
|
70
|
+
'report.title': '研究报告',
|
|
71
|
+
'report.new': '新报告',
|
|
72
|
+
'report.body': '报告正文',
|
|
73
|
+
'report.save': '确认报告',
|
|
74
|
+
'report.history': '历史版本',
|
|
75
|
+
'report.missing': '原件不可用,仍可核对上面的摘录。',
|
|
76
|
+
'report.missingEvidence': '这条证据已不可用。',
|
|
77
|
+
'source.title': '研究资料',
|
|
78
|
+
'source.import': '导入 Markdown 或文本 PDF',
|
|
79
|
+
'source.read': '看看原文',
|
|
80
|
+
'source.save': '保存为证据',
|
|
81
|
+
'source.saved': '证据已保存',
|
|
82
|
+
'source.open': '打开原始文件',
|
|
83
|
+
'research.title': '研究',
|
|
84
|
+
'research.new': '新建独立研究',
|
|
85
|
+
'research.question': '想深挖什么?',
|
|
86
|
+
'research.notes': '研究记录',
|
|
87
|
+
'research.save': '保存研究',
|
|
88
|
+
'research.continue': '接着上次深挖',
|
|
89
|
+
'research.back': '返回首页',
|
|
90
|
+
'research.saved': '已保存。',
|
|
91
|
+
'idea.title': '灵感来了',
|
|
92
|
+
'idea.placeholder': '刚冒出什么点子?先记一句。',
|
|
93
|
+
'idea.save': '💡叮~',
|
|
94
|
+
'idea.saved': '叮~ 记住啦!',
|
|
95
|
+
'idea.find': '找回灵感',
|
|
96
|
+
'idea.search': '搜索',
|
|
97
|
+
'idea.empty': '没找到这条灵感。',
|
|
98
|
+
'idea.version': '历史版本',
|
|
26
99
|
panel: 'Grove',
|
|
27
100
|
menu: '知林',
|
|
28
101
|
subtitle: '把探索留在自己的 vault 里。',
|
|
@@ -43,31 +116,7 @@ window.__ModuleLoader__.load({
|
|
|
43
116
|
's0.hint': '路径会保存到当前 profile 的插件配置;之后可在插件页里修改。',
|
|
44
117
|
's0.stale': '原来的 vault 已不可用:{path}。请重新选择,或把它移回原位。',
|
|
45
118
|
's0.noVault': '这个文件夹里没有 .obsidian。如果它不是 vault 根目录,仍可继续连接。',
|
|
46
|
-
's1.connectedTo': '已连接',
|
|
47
119
|
'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
120
|
'type.inspiration': '灵感',
|
|
72
121
|
'type.question': '问题',
|
|
73
122
|
'type.concept': '概念',
|
|
@@ -97,6 +146,69 @@ window.__ModuleLoader__.load({
|
|
|
97
146
|
}
|
|
98
147
|
|
|
99
148
|
var EN = {
|
|
149
|
+
'asset.knowledge': 'Knowledge',
|
|
150
|
+
'asset.works': 'Works',
|
|
151
|
+
'asset.title': 'Title',
|
|
152
|
+
'asset.body': 'Body',
|
|
153
|
+
'asset.new': 'New',
|
|
154
|
+
'asset.save': 'Save',
|
|
155
|
+
'asset.basis': 'View basis',
|
|
156
|
+
'asset.derive': 'Save as knowledge',
|
|
157
|
+
'asset.compose': 'Create a work',
|
|
158
|
+
'asset.research': 'Research from this knowledge',
|
|
159
|
+
'asset.reason': 'Reflection · What changed your understanding?',
|
|
160
|
+
'asset.copy': 'Copy body',
|
|
161
|
+
'asset.copied': 'Copied',
|
|
162
|
+
'asset.type': 'Knowledge type',
|
|
163
|
+
'asset.missing': 'The source is unavailable.',
|
|
164
|
+
'report.review': 'Review as report draft',
|
|
165
|
+
'report.discard': 'Discard unsaved draft',
|
|
166
|
+
'model.title': 'Research model',
|
|
167
|
+
'model.enabled': 'Enable AI research',
|
|
168
|
+
'model.endpoint': 'HTTPS model endpoint',
|
|
169
|
+
'model.name': 'Model name',
|
|
170
|
+
'model.test': 'Test connection',
|
|
171
|
+
'model.apply': 'Apply model settings',
|
|
172
|
+
'model.tested': 'Connection verified.',
|
|
173
|
+
'model.ask': 'Ask AI to investigate',
|
|
174
|
+
'model.question': 'What should we investigate?',
|
|
175
|
+
'model.scope': 'Only selected excerpts and earlier findings in this study are sent.',
|
|
176
|
+
'parsers.title': 'Parser settings',
|
|
177
|
+
'parsers.pdf': 'Enable text PDF parsing',
|
|
178
|
+
'parsers.apply': 'Apply settings',
|
|
179
|
+
'parsers.pending': 'Applying…',
|
|
180
|
+
'parsers.done': 'Parser settings applied.',
|
|
181
|
+
'parsers.failed': 'Settings did not take effect. Try again.',
|
|
182
|
+
'parsers.hint': 'Markdown remains available. Existing evidence stays readable when PDF parsing is off.',
|
|
183
|
+
'report.title': 'Research reports',
|
|
184
|
+
'report.new': 'New report',
|
|
185
|
+
'report.body': 'Report body',
|
|
186
|
+
'report.save': 'Confirm report',
|
|
187
|
+
'report.history': 'History',
|
|
188
|
+
'report.missing': 'Original unavailable. The saved excerpt is shown above.',
|
|
189
|
+
'report.missingEvidence': 'This evidence version is unavailable.',
|
|
190
|
+
'source.title': 'Sources',
|
|
191
|
+
'source.import': 'Import Markdown or text PDF',
|
|
192
|
+
'source.read': 'View original passage',
|
|
193
|
+
'source.save': 'Save as evidence',
|
|
194
|
+
'source.saved': 'Evidence saved',
|
|
195
|
+
'source.open': 'Open original file',
|
|
196
|
+
'research.title': 'Research',
|
|
197
|
+
'research.new': 'New independent study',
|
|
198
|
+
'research.question': 'What do you want to explore?',
|
|
199
|
+
'research.notes': 'Research notes',
|
|
200
|
+
'research.save': 'Save study',
|
|
201
|
+
'research.continue': 'Continue the last study',
|
|
202
|
+
'research.back': 'Back to home',
|
|
203
|
+
'research.saved': 'Saved.',
|
|
204
|
+
'idea.title': 'An idea?',
|
|
205
|
+
'idea.placeholder': 'What just came to mind? Jot it down.',
|
|
206
|
+
'idea.save': '💡 Ding!',
|
|
207
|
+
'idea.saved': 'Ding! Saved.',
|
|
208
|
+
'idea.find': 'Find ideas',
|
|
209
|
+
'idea.search': 'Search',
|
|
210
|
+
'idea.empty': 'No matching ideas.',
|
|
211
|
+
'idea.version': 'History',
|
|
100
212
|
panel: 'Grove',
|
|
101
213
|
menu: 'Grove',
|
|
102
214
|
subtitle: 'Keep what you explore in your own vault.',
|
|
@@ -117,31 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
117
229
|
's0.hint': 'The path is saved in this profile’s plugin configuration and can be changed later from the Plugins page.',
|
|
118
230
|
's0.stale': 'The previous vault is no longer usable: {path}. Choose it again, or move it back.',
|
|
119
231
|
's0.noVault': 'This folder has no .obsidian. You can still connect if it is a vault root.',
|
|
120
|
-
's1.connectedTo': 'Connected',
|
|
121
232
|
'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
233
|
'type.inspiration': 'Inspiration',
|
|
146
234
|
'type.question': 'Question',
|
|
147
235
|
'type.concept': 'Concept',
|
|
@@ -214,18 +302,6 @@ window.__ModuleLoader__.load({
|
|
|
214
302
|
return (error && error.message) || T('err.internal')
|
|
215
303
|
}
|
|
216
304
|
|
|
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
305
|
function waitForVault(expected, tries) {
|
|
230
306
|
return api(STATUS).catch(function () { return null }).then(function (status) {
|
|
231
307
|
if (status && status.configured && status.usable && (expected === undefined || status.vault === expected)) return status
|
|
@@ -249,9 +325,18 @@ window.__ModuleLoader__.load({
|
|
|
249
325
|
var [manual, setManual] = useState(false)
|
|
250
326
|
var [draft, setDraft] = useState('')
|
|
251
327
|
var [pathDraft, setPathDraft] = useState('')
|
|
252
|
-
var [
|
|
253
|
-
var [
|
|
328
|
+
var [query, setQuery] = useState('')
|
|
329
|
+
var [notes, setNotes] = useState(null)
|
|
330
|
+
var [selected, setSelected] = useState(null)
|
|
331
|
+
var [saved, setSaved] = useState(false)
|
|
254
332
|
var [changing, setChanging] = useState(false)
|
|
333
|
+
var [page, setPage] = useState('home')
|
|
334
|
+
var [seed, setSeed] = useState(null)
|
|
335
|
+
|
|
336
|
+
function navigate(destination, draft) {
|
|
337
|
+
setSeed(draft || null)
|
|
338
|
+
setPage(destination)
|
|
339
|
+
}
|
|
255
340
|
var mounted = useRef(true)
|
|
256
341
|
|
|
257
342
|
useEffect(function () {
|
|
@@ -277,11 +362,17 @@ window.__ModuleLoader__.load({
|
|
|
277
362
|
function connect(path) {
|
|
278
363
|
setBusy('connect')
|
|
279
364
|
setError(null)
|
|
280
|
-
return api(
|
|
281
|
-
return
|
|
365
|
+
return api(CONNECTION, { path: path }).then(function (candidate) {
|
|
366
|
+
return api(CONFIG, { path: candidate.path }).then(function () {
|
|
367
|
+
return waitForVault(candidate.path, 12)
|
|
368
|
+
})
|
|
282
369
|
}).then(function (next) {
|
|
283
370
|
if (!mounted.current) return
|
|
284
371
|
setStatus(next)
|
|
372
|
+
navigate('home')
|
|
373
|
+
setNotes(null)
|
|
374
|
+
setSelected(null)
|
|
375
|
+
setSaved(false)
|
|
285
376
|
setManual(false)
|
|
286
377
|
setChanging(false)
|
|
287
378
|
}, function (problem) {
|
|
@@ -319,37 +410,39 @@ window.__ModuleLoader__.load({
|
|
|
319
410
|
connect(path)
|
|
320
411
|
}
|
|
321
412
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
setBusy('capture')
|
|
413
|
+
function requestNotes(input, apply) {
|
|
414
|
+
if (busy) return
|
|
415
|
+
setBusy('read')
|
|
326
416
|
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
|
-
})
|
|
417
|
+
return api(INSPIRATIONS, input).then(function (data) {
|
|
418
|
+
if (mounted.current) apply(data)
|
|
334
419
|
}, function (problem) {
|
|
335
420
|
if (mounted.current) setError(problem)
|
|
336
|
-
}).
|
|
421
|
+
}).finally(function () {
|
|
337
422
|
if (mounted.current) setBusy('')
|
|
338
423
|
})
|
|
339
424
|
}
|
|
340
425
|
|
|
341
|
-
function
|
|
342
|
-
|
|
426
|
+
function saveIdea() {
|
|
427
|
+
var body = draft.trim()
|
|
428
|
+
if (!body || busy) return
|
|
429
|
+
setBusy('capture')
|
|
430
|
+
setError(null)
|
|
431
|
+
setSaved(false)
|
|
432
|
+
api(CAPTURE, { body: body }).then(function () {
|
|
343
433
|
if (!mounted.current) return
|
|
344
|
-
|
|
345
|
-
|
|
434
|
+
setDraft('')
|
|
435
|
+
setSaved(true)
|
|
436
|
+
setNotes(null)
|
|
437
|
+
setSelected(null)
|
|
346
438
|
}, function (problem) {
|
|
347
439
|
if (mounted.current) setError(problem)
|
|
440
|
+
}).finally(function () {
|
|
441
|
+
if (mounted.current) setBusy('')
|
|
348
442
|
})
|
|
349
443
|
}
|
|
350
444
|
|
|
351
445
|
var connected = !!(status && status.configured && status.usable)
|
|
352
|
-
var total = status && status.counts ? Number(status.counts.total) || 0 : 0
|
|
353
446
|
var connectView = h(Connect, {
|
|
354
447
|
busy: busy, manual: manual, error: error, pathDraft: pathDraft,
|
|
355
448
|
stale: !changing && status && status.configured && !status.usable ? status.vault : null,
|
|
@@ -376,12 +469,23 @@ window.__ModuleLoader__.load({
|
|
|
376
469
|
? h('div', { className: 'gr-card' }, h('div', { className: 'gr-body' }, T('loading')))
|
|
377
470
|
: changing || !connected
|
|
378
471
|
? connectView
|
|
379
|
-
:
|
|
380
|
-
? h(
|
|
381
|
-
:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
472
|
+
: page === 'research'
|
|
473
|
+
? h(ResearchPage, { seed: seed, onNavigate: navigate, onBack: function () { navigate('home') } })
|
|
474
|
+
: page === 'knowledge' || page === 'works'
|
|
475
|
+
? h(AssetPage, { key: page, kind: page, seed: seed, onNavigate: navigate })
|
|
476
|
+
: h(InspirationHome, {
|
|
477
|
+
onResearch: function () { navigate('research') },
|
|
478
|
+
onKnowledge: function () { navigate('knowledge') },
|
|
479
|
+
onWorks: function () { navigate('works') },
|
|
480
|
+
draft: draft, busy: busy, saved: saved, onSave: saveIdea,
|
|
481
|
+
onDraft: function (value) { setDraft(value); setSaved(false) },
|
|
482
|
+
query: query, notes: notes, selected: selected, onQuery: setQuery,
|
|
483
|
+
onSearch: function () {
|
|
484
|
+
requestNotes({ query: query }, function (data) { setNotes(data.notes); setSelected(null) })
|
|
485
|
+
},
|
|
486
|
+
onSelect: function (id, version) {
|
|
487
|
+
requestNotes({ id: id, version: version }, setSelected)
|
|
488
|
+
},
|
|
385
489
|
}),
|
|
386
490
|
h(Footer, null),
|
|
387
491
|
),
|
|
@@ -444,78 +548,591 @@ window.__ModuleLoader__.load({
|
|
|
444
548
|
)
|
|
445
549
|
}
|
|
446
550
|
|
|
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
|
-
}
|
|
551
|
+
function InspirationHome(props) {
|
|
552
|
+
var selected = props.selected
|
|
457
553
|
return h(React.Fragment, null,
|
|
458
554
|
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')),
|
|
555
|
+
h('label', { className: 'gr-h2', htmlFor: 'gr-idea' }, T('idea.title')),
|
|
462
556
|
h('textarea', {
|
|
463
|
-
className: 'gr-textarea', value: props.draft,
|
|
464
|
-
|
|
557
|
+
id: 'gr-idea', className: 'gr-textarea', value: props.draft,
|
|
558
|
+
placeholder: T('idea.placeholder'),
|
|
465
559
|
onChange: function (event) { props.onDraft(event.target.value) },
|
|
466
560
|
onKeyDown: function (event) {
|
|
467
561
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') props.onSave()
|
|
468
562
|
},
|
|
469
563
|
}),
|
|
470
564
|
h('div', { className: 'gr-row' },
|
|
471
|
-
h('button', { className: 'gr-btn', disabled: props.busy
|
|
472
|
-
|
|
473
|
-
h('span', { className: 'gr-meta' }, '⌘↵'),
|
|
565
|
+
h('button', { className: 'gr-btn', disabled: !!props.busy || !props.draft.trim(), onClick: props.onSave }, T('idea.save')),
|
|
566
|
+
h('span', { role: 'status' }, props.saved ? T('idea.saved') : ''),
|
|
474
567
|
),
|
|
475
568
|
),
|
|
476
|
-
h('div', { className: 'gr-
|
|
477
|
-
h('
|
|
478
|
-
h('
|
|
479
|
-
h('button', { className: 'gr-btn gr-btn-ghost
|
|
569
|
+
h('div', { className: 'gr-row' },
|
|
570
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onResearch }, T('research.title')),
|
|
571
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onKnowledge }, T('asset.knowledge')),
|
|
572
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy, onClick: props.onWorks }, T('asset.works'))),
|
|
573
|
+
h('details', { className: 'gr-card' },
|
|
574
|
+
h('summary', null, T('idea.find')),
|
|
575
|
+
h('form', { className: 'gr-row', onSubmit: function (event) {
|
|
576
|
+
event.preventDefault()
|
|
577
|
+
props.onSearch()
|
|
578
|
+
} },
|
|
579
|
+
h('input', { className: 'gr-input', type: 'search', 'aria-label': T('idea.find'),
|
|
580
|
+
value: props.query, onChange: function (event) { props.onQuery(event.target.value) } }),
|
|
581
|
+
h('button', { className: 'gr-btn gr-btn-sm', disabled: !!props.busy }, T('idea.search'))),
|
|
582
|
+
props.notes && props.notes.length === 0 ? h('p', null, T('idea.empty')) : null,
|
|
583
|
+
(props.notes || []).map(function (note) {
|
|
584
|
+
return h('div', { key: note.id },
|
|
585
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: !!props.busy,
|
|
586
|
+
onClick: function () { props.onSelect(note.id, note.version) } }, note.title))
|
|
587
|
+
}),
|
|
588
|
+
selected ? h('article', { className: 'gr-card' },
|
|
589
|
+
h('div', { className: 'gr-h2' }, selected.meta.title),
|
|
590
|
+
h('pre', { className: 'gr-code' }, selected.body),
|
|
591
|
+
h('label', null, T('idea.version'), ' ',
|
|
592
|
+
h('select', { value: selected.meta.version, disabled: !!props.busy,
|
|
593
|
+
onChange: function (event) { props.onSelect(selected.meta.id, Number(event.target.value)) } },
|
|
594
|
+
selected.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))),
|
|
595
|
+
) : null,
|
|
480
596
|
),
|
|
481
597
|
)
|
|
482
598
|
}
|
|
483
599
|
|
|
484
|
-
function
|
|
485
|
-
var
|
|
486
|
-
var
|
|
487
|
-
var
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
|
|
600
|
+
function ModelSettings() {
|
|
601
|
+
var [config, setConfig] = useState(null)
|
|
602
|
+
var [busy, setBusy] = useState(false)
|
|
603
|
+
var [message, setMessage] = useState('')
|
|
604
|
+
var [error, setError] = useState(null)
|
|
605
|
+
|
|
606
|
+
function change(key, value) {
|
|
607
|
+
setConfig(function (current) { return Object.assign({}, current, { [key]: value }) })
|
|
608
|
+
setMessage('')
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function submit(event) {
|
|
612
|
+
event.preventDefault()
|
|
613
|
+
var test = event.nativeEvent.submitter?.value === 'test'
|
|
614
|
+
setBusy(true)
|
|
615
|
+
setError(null)
|
|
616
|
+
setMessage('')
|
|
617
|
+
try {
|
|
618
|
+
await api(MODEL, Object.assign({}, config, { test: test }))
|
|
619
|
+
if (test) { setMessage(T('model.tested')); return }
|
|
620
|
+
for (var attempt = 0; attempt < 12; attempt++) {
|
|
621
|
+
await new Promise(function (resolve) { setTimeout(resolve, 400) })
|
|
622
|
+
var current = await api(MODEL).catch(function () { return null })
|
|
623
|
+
if (current && current.enabled === config.enabled &&
|
|
624
|
+
(!current.enabled || (current.name === config.name && current.endpoint === config.endpoint))) {
|
|
625
|
+
setMessage(T('research.saved'))
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
throw new Error(T('parsers.failed'))
|
|
630
|
+
} catch (problem) { setError(problem) }
|
|
631
|
+
finally { setBusy(false) }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return h('details', { className: 'gr-card', onToggle: function (event) {
|
|
635
|
+
if (event.currentTarget.open && !config) api(MODEL).then(setConfig, setError)
|
|
636
|
+
} },
|
|
637
|
+
h('summary', null, T('model.title')),
|
|
638
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
639
|
+
config ? h('form', { onSubmit: submit },
|
|
640
|
+
h('label', null, h('input', { type: 'checkbox', checked: config.enabled, disabled: busy,
|
|
641
|
+
onChange: function (event) { change('enabled', event.target.checked) } }), T('model.enabled')),
|
|
642
|
+
config.enabled ? h(React.Fragment, null,
|
|
643
|
+
h('label', null, T('model.endpoint'), h('input', { type: 'url', className: 'gr-input', value: config.endpoint || '',
|
|
644
|
+
required: true, disabled: busy, onChange: function (event) { change('endpoint', event.target.value) } })),
|
|
645
|
+
h('label', null, T('model.name'), h('input', { className: 'gr-input', value: config.name || '',
|
|
646
|
+
required: true, disabled: busy, onChange: function (event) { change('name', event.target.value) } })),
|
|
647
|
+
h('p', { className: 'gr-meta' }, 'GROVE_MODEL_KEY')) : null,
|
|
648
|
+
h('div', { className: 'gr-row' },
|
|
649
|
+
config.enabled ? h('button', { className: 'gr-btn', value: 'test', disabled: busy }, T('model.test')) : null,
|
|
650
|
+
h('button', { className: 'gr-btn', value: 'apply', disabled: busy }, T('model.apply'))),
|
|
651
|
+
h('p', { role: 'status' }, message)) : null)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function ResearchFindings(props) {
|
|
655
|
+
var [question, setQuestion] = useState(props.title)
|
|
656
|
+
var [findings, setFindings] = useState([])
|
|
657
|
+
var [evidence, setEvidence] = useState([])
|
|
658
|
+
var [selected, setSelected] = useState([])
|
|
659
|
+
var [busy, setBusy] = useState(false)
|
|
660
|
+
var [error, setError] = useState(null)
|
|
661
|
+
|
|
662
|
+
function load(event) {
|
|
663
|
+
if (!event.currentTarget.open) return
|
|
664
|
+
api(FINDINGS, { action: 'list', id: props.id }).then(function (result) { setFindings(result.findings) }, setError)
|
|
665
|
+
api(REPORTS, { action: 'list', research: props.id }).then(function (result) { setEvidence(result.evidence) }, setError)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function ask(event) {
|
|
669
|
+
event.preventDefault()
|
|
670
|
+
setBusy(true)
|
|
671
|
+
setError(null)
|
|
672
|
+
try {
|
|
673
|
+
var finding = await api(FINDINGS, { id: props.id, version: props.version, question: question,
|
|
674
|
+
includeHistory: true, materials: evidence.filter(function (item) { return selected.includes(item.id) }).map(function (item) {
|
|
675
|
+
return { id: item.id, version: item.version }
|
|
676
|
+
}) })
|
|
677
|
+
setFindings(function (items) { return items.concat(finding) })
|
|
678
|
+
setQuestion('')
|
|
679
|
+
} catch (problem) { setError(problem) }
|
|
680
|
+
finally { setBusy(false) }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return h('details', { className: 'gr-card', onToggle: load },
|
|
684
|
+
h('summary', null, T('model.ask')),
|
|
685
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
686
|
+
findings.map(function (item) {
|
|
687
|
+
return h('article', { key: item.meta.id }, h('h3', null, item.meta.title), h('pre', { className: 'gr-code' }, item.body),
|
|
688
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
689
|
+
api(REPORTS, { action: 'draft', research: props.id, id: item.meta.id, version: item.meta.version }).then(props.onDraft, setError)
|
|
690
|
+
} }, T('report.review')))
|
|
691
|
+
}),
|
|
692
|
+
h('form', { onSubmit: ask },
|
|
693
|
+
h('label', null, T('model.question'), h('textarea', { className: 'gr-textarea', value: question,
|
|
694
|
+
required: true, maxLength: 1000, onChange: function (event) { setQuestion(event.target.value) } })),
|
|
695
|
+
h('p', { className: 'gr-meta' }, T('model.scope')),
|
|
696
|
+
evidence.map(function (item) {
|
|
697
|
+
return h('label', { key: item.id }, h('input', { type: 'checkbox', checked: selected.includes(item.id),
|
|
698
|
+
onChange: function (event) { setSelected(event.target.checked ? selected.concat(item.id) : selected.filter(function (id) { return id !== item.id })) } }), item.title)
|
|
699
|
+
}),
|
|
700
|
+
h('button', { className: 'gr-btn', disabled: busy }, T('model.ask'))))
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function ParserSettings() {
|
|
704
|
+
var [enabled, setEnabled] = useState(null)
|
|
705
|
+
var [pending, setPending] = useState(false)
|
|
706
|
+
var [done, setDone] = useState(false)
|
|
707
|
+
var [error, setError] = useState(null)
|
|
708
|
+
|
|
709
|
+
async function apply(event) {
|
|
710
|
+
event.preventDefault()
|
|
711
|
+
setPending(true)
|
|
712
|
+
setDone(false)
|
|
713
|
+
setError(null)
|
|
714
|
+
try {
|
|
715
|
+
await api(PARSERS, { pdf: enabled })
|
|
716
|
+
for (var attempt = 0; attempt < 12; attempt++) {
|
|
717
|
+
await new Promise(function (resolve) { setTimeout(resolve, 400) })
|
|
718
|
+
var current = await api(PARSERS).catch(function () { return null })
|
|
719
|
+
if (current && current.pdf === enabled) {
|
|
720
|
+
setDone(true)
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
throw new Error(T('parsers.failed'))
|
|
725
|
+
} catch (problem) {
|
|
726
|
+
setError(problem)
|
|
727
|
+
} finally {
|
|
728
|
+
setPending(false)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return h('details', { className: 'gr-card', onToggle: function (event) {
|
|
733
|
+
if (!event.currentTarget.open || enabled !== null) return
|
|
734
|
+
api(PARSERS).then(function (current) { setEnabled(current.pdf); setError(null) }, setError)
|
|
735
|
+
} },
|
|
736
|
+
h('summary', null, T('parsers.title')),
|
|
737
|
+
h('p', { className: 'gr-body' }, T('parsers.hint')),
|
|
738
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
739
|
+
enabled === null ? null : h('form', { onSubmit: apply },
|
|
740
|
+
h('label', null, h('input', { type: 'checkbox', checked: enabled, disabled: pending,
|
|
741
|
+
onChange: function (event) { setEnabled(event.target.checked); setDone(false) } }), T('parsers.pdf')),
|
|
742
|
+
h('button', { className: 'gr-btn', disabled: pending }, T(pending ? 'parsers.pending' : 'parsers.apply')),
|
|
743
|
+
h('p', { role: 'status' }, done ? T('parsers.done') : '')))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function readSourceFile(file) {
|
|
747
|
+
if (file.size > 500000) throw new Error(T('err.too-large'))
|
|
748
|
+
var bytes = new Uint8Array(await file.arrayBuffer())
|
|
749
|
+
var binary = ''
|
|
750
|
+
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
|
|
751
|
+
return btoa(binary)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function ResearchSources(props) {
|
|
755
|
+
var [items, setItems] = useState([])
|
|
756
|
+
var [document, setDocument] = useState(null)
|
|
757
|
+
var [busy, setBusy] = useState(false)
|
|
758
|
+
var [error, setError] = useState(null)
|
|
759
|
+
var [saved, setSaved] = useState(false)
|
|
760
|
+
useEffect(function () {
|
|
761
|
+
var active = true
|
|
762
|
+
api(SOURCES, { action: 'list', research: props.id }).then(function (value) {
|
|
763
|
+
if (active) setItems(value.sources)
|
|
764
|
+
}, function (problem) {
|
|
765
|
+
if (active) setError(problem)
|
|
766
|
+
})
|
|
767
|
+
return function () { active = false }
|
|
768
|
+
}, [props.id])
|
|
769
|
+
|
|
770
|
+
async function run(operation) {
|
|
771
|
+
setBusy(true)
|
|
772
|
+
setError(null)
|
|
773
|
+
setSaved(false)
|
|
774
|
+
try { await operation() }
|
|
775
|
+
catch (problem) { setError(problem) }
|
|
776
|
+
finally { setBusy(false) }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function upload(event) {
|
|
780
|
+
var file = event.target.files[0]
|
|
781
|
+
event.target.value = ''
|
|
782
|
+
if (!file) return
|
|
783
|
+
await run(async function () {
|
|
784
|
+
var original = await readSourceFile(file)
|
|
785
|
+
var result = await api(SOURCES, { action: 'import', name: file.name, original: original, research: props.id })
|
|
786
|
+
setItems(function (existing) { return [result.meta].concat(existing.filter(function (item) { return item.id !== result.meta.id })) })
|
|
787
|
+
setDocument({ meta: result.meta, blocks: result.blocks, original: original })
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function download() {
|
|
792
|
+
var bytes = Uint8Array.from(atob(document.original), function (character) { return character.charCodeAt(0) })
|
|
793
|
+
var url = URL.createObjectURL(new Blob([bytes]))
|
|
794
|
+
var link = window.document.createElement('a')
|
|
795
|
+
link.href = url
|
|
796
|
+
link.download = document.meta.title
|
|
797
|
+
link.click()
|
|
798
|
+
setTimeout(function () { URL.revokeObjectURL(url) }, 1000)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return h('section', { className: 'gr-card' },
|
|
802
|
+
h('h2', { className: 'gr-h2' }, T('source.title')),
|
|
803
|
+
h('label', null, T('source.import'), ' ',
|
|
804
|
+
h('input', { type: 'file', accept: '.md,.markdown,.pdf', disabled: busy, onChange: upload })),
|
|
805
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
806
|
+
h('p', { role: 'status' }, saved ? T('source.saved') : ''),
|
|
807
|
+
items.map(function (item) {
|
|
808
|
+
return h('button', { key: item.id, className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
809
|
+
run(async function () { setDocument(await api(SOURCES, { action: 'open', id: item.id, version: item.version })) })
|
|
810
|
+
} }, item.title)
|
|
811
|
+
}),
|
|
812
|
+
document ? h('div', null,
|
|
813
|
+
h('button', { className: 'gr-btn', onClick: download }, T('source.open')),
|
|
814
|
+
document.blocks.map(function (block, index) {
|
|
815
|
+
return h('details', { key: document.meta.id + ':' + index },
|
|
816
|
+
h('summary', null, T('source.read'), ' · ', block.locator),
|
|
817
|
+
h('pre', { className: 'gr-code' }, block.text),
|
|
818
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: function () {
|
|
819
|
+
run(async function () {
|
|
820
|
+
await api(SOURCES, { action: 'excerpt', id: document.meta.id, version: document.meta.version, index: index })
|
|
821
|
+
setSaved(true)
|
|
822
|
+
})
|
|
823
|
+
} }, T('source.save')))
|
|
824
|
+
})) : null)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function ResearchReports(props) {
|
|
828
|
+
var [catalog, setCatalog] = useState(null)
|
|
829
|
+
var [report, setReport] = useState(null)
|
|
830
|
+
var [body, setBody] = useState('')
|
|
831
|
+
var [references, setReferences] = useState([])
|
|
832
|
+
var [originals, setOriginals] = useState({})
|
|
833
|
+
var [busy, setBusy] = useState(false)
|
|
834
|
+
var [error, setError] = useState(null)
|
|
835
|
+
var [editing, setEditing] = useState(false)
|
|
836
|
+
|
|
837
|
+
useEffect(function () {
|
|
838
|
+
if (!props.draft) return
|
|
839
|
+
setBody(props.draft.body)
|
|
840
|
+
setReferences(props.draft.evidence)
|
|
841
|
+
setReport(null)
|
|
842
|
+
setEditing(true)
|
|
843
|
+
list().catch(setError)
|
|
844
|
+
}, [props.draft])
|
|
845
|
+
|
|
846
|
+
function list() {
|
|
847
|
+
return api(REPORTS, { action: 'list', research: props.id }).then(setCatalog)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function run(operation) {
|
|
851
|
+
setBusy(true)
|
|
852
|
+
setError(null)
|
|
853
|
+
try { await operation() }
|
|
854
|
+
catch (problem) { setError(problem) }
|
|
855
|
+
finally { setBusy(false) }
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function read(id, version) {
|
|
859
|
+
var result = await api(REPORTS, { action: 'read', id: id, version: version })
|
|
860
|
+
setReport(result)
|
|
861
|
+
setBody(result.body)
|
|
862
|
+
setReferences(result.evidence.filter(function (item) { return !item.missing }).map(function (item) {
|
|
863
|
+
return { id: item.meta.id, version: item.meta.version }
|
|
864
|
+
}))
|
|
865
|
+
setOriginals({})
|
|
866
|
+
setEditing(true)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function start() {
|
|
870
|
+
setReport(null)
|
|
871
|
+
setBody('')
|
|
872
|
+
setReferences([])
|
|
873
|
+
setEditing(true)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function save(event) {
|
|
877
|
+
event.preventDefault()
|
|
878
|
+
run(async function () {
|
|
879
|
+
var input = { action: 'confirm', research: props.id, body: body, evidence: references }
|
|
880
|
+
if (report) { input.id = report.meta.id; input.version = report.meta.version }
|
|
881
|
+
var result = await api(REPORTS, input)
|
|
882
|
+
await read(result.meta.id, result.meta.version)
|
|
883
|
+
await list()
|
|
884
|
+
})
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function cite(item) {
|
|
888
|
+
if (item.missing) return h('p', { key: item.reference.id }, T('report.missingEvidence'))
|
|
889
|
+
var ref = item.original
|
|
890
|
+
return h('div', { key: item.meta.id, className: 'gr-card' },
|
|
891
|
+
h('p', null, item.meta.source, ' · ', item.meta.locator),
|
|
892
|
+
h('pre', { className: 'gr-code' }, item.body),
|
|
893
|
+
ref ? h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () {
|
|
894
|
+
run(async function () {
|
|
895
|
+
var text
|
|
896
|
+
try {
|
|
897
|
+
var result = await api(SOURCES, { action: 'open', id: ref.id, version: ref.version })
|
|
898
|
+
var block = result.blocks.find(function (entry) { return entry.locator === item.meta.locator })
|
|
899
|
+
text = block ? block.text : T('report.missing')
|
|
900
|
+
} catch { text = T('report.missing') }
|
|
901
|
+
setOriginals(function (previous) { return Object.assign({}, previous, { [item.meta.id]: text }) })
|
|
902
|
+
})
|
|
903
|
+
} }, T('source.read')) : null,
|
|
904
|
+
originals[item.meta.id] ? h('pre', { className: 'gr-code' }, originals[item.meta.id]) : null)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return h('details', { className: 'gr-card', open: props.draft ? true : undefined, onToggle: function (event) {
|
|
908
|
+
if (event.currentTarget.open) run(list)
|
|
909
|
+
} },
|
|
910
|
+
h('summary', null, T('report.title')),
|
|
911
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
912
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: start }, T('report.new')),
|
|
913
|
+
(catalog ? catalog.reports : []).map(function (entry) {
|
|
914
|
+
return h('button', { key: entry.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
915
|
+
onClick: function () { run(function () { return read(entry.meta.id, entry.meta.version) }) } }, entry.meta.title)
|
|
916
|
+
}),
|
|
917
|
+
report ? h('label', null, T('report.history'), ' ', h('select', {
|
|
918
|
+
value: report.meta.version, disabled: busy,
|
|
919
|
+
onChange: function (event) {
|
|
920
|
+
var version = Number(event.target.value)
|
|
921
|
+
run(function () { return read(report.meta.id, version) })
|
|
922
|
+
},
|
|
923
|
+
}, report.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))) : null,
|
|
924
|
+
report ? report.evidence.map(cite) : null,
|
|
925
|
+
report ? h('div', { className: 'gr-row' },
|
|
926
|
+
h('button', { className: 'gr-btn', onClick: function () {
|
|
927
|
+
props.onNavigate('knowledge', { title: report.meta.title, body: report.body, report: { id: report.meta.id, version: report.meta.version } })
|
|
928
|
+
} }, T('asset.derive')),
|
|
929
|
+
h('button', { className: 'gr-btn', onClick: function () {
|
|
930
|
+
props.onNavigate('works', { title: report.meta.title, body: report.body, basis: { id: report.meta.id, version: report.meta.version } })
|
|
931
|
+
} }, T('asset.compose'))) : null,
|
|
932
|
+
editing ? h('form', { onSubmit: save },
|
|
933
|
+
h('label', null, T('report.body'), h('textarea', { className: 'gr-textarea', required: true,
|
|
934
|
+
value: body, onChange: function (event) { setBody(event.target.value) } })),
|
|
935
|
+
(catalog ? catalog.evidence : []).map(function (item) {
|
|
936
|
+
var checked = references.some(function (ref) { return ref.id === item.id && ref.version === item.version })
|
|
937
|
+
return h('label', { key: item.id }, h('input', { type: 'checkbox', checked: checked,
|
|
938
|
+
onChange: function () {
|
|
939
|
+
setReferences(checked ? references.filter(function (ref) { return ref.id !== item.id })
|
|
940
|
+
: references.filter(function (ref) { return ref.id !== item.id }).concat({ id: item.id, version: item.version }))
|
|
941
|
+
} }), item.title, ' · v', item.version)
|
|
942
|
+
}),
|
|
943
|
+
h('button', { className: 'gr-btn', disabled: busy || (report && report.meta.version !== report.versions[0]) }, T('report.save')),
|
|
944
|
+
h('button', { type: 'button', className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
945
|
+
onClick: function () { setEditing(false); props.onDiscard() } }, T('report.discard'))) : null)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function AssetPage(props) {
|
|
949
|
+
var isKnowledge = props.kind === 'knowledge'
|
|
950
|
+
var endpoint = isKnowledge ? KNOWLEDGE : WORKS
|
|
951
|
+
var [items, setItems] = useState([])
|
|
952
|
+
var [selected, setSelected] = useState(null)
|
|
953
|
+
var [draft, setDraft] = useState(props.seed)
|
|
954
|
+
var [title, setTitle] = useState(props.seed?.title || '')
|
|
955
|
+
var [body, setBody] = useState(props.seed?.body || '')
|
|
956
|
+
var [kind, setKind] = useState('concept')
|
|
957
|
+
var [reason, setReason] = useState('')
|
|
958
|
+
var [basis, setBasis] = useState(null)
|
|
959
|
+
var [busy, setBusy] = useState(false)
|
|
960
|
+
var [error, setError] = useState(null)
|
|
961
|
+
var [message, setMessage] = useState('')
|
|
962
|
+
|
|
963
|
+
function list() { return api(endpoint, { action: 'list' }).then(function (result) { setItems(result.items) }) }
|
|
964
|
+
useEffect(function () { list().catch(setError) }, [endpoint])
|
|
965
|
+
|
|
966
|
+
async function run(operation) {
|
|
967
|
+
setBusy(true)
|
|
968
|
+
setError(null)
|
|
969
|
+
setMessage('')
|
|
970
|
+
try { await operation() }
|
|
971
|
+
catch (problem) { setError(problem) }
|
|
972
|
+
finally { setBusy(false) }
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function read(id, version) {
|
|
976
|
+
var entry = await api(endpoint, { action: 'read', id: id, version: version })
|
|
977
|
+
setSelected(entry)
|
|
978
|
+
setDraft({})
|
|
979
|
+
setTitle(entry.meta.title)
|
|
980
|
+
setBody(entry.body)
|
|
981
|
+
setBasis(null)
|
|
982
|
+
setReason('')
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function start() {
|
|
986
|
+
setSelected(null)
|
|
987
|
+
setDraft({})
|
|
988
|
+
setTitle('')
|
|
989
|
+
setBody('')
|
|
990
|
+
setBasis(null)
|
|
991
|
+
setReason('')
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function save(event) {
|
|
995
|
+
event.preventDefault()
|
|
996
|
+
run(async function () {
|
|
997
|
+
var input = { action: 'save', body: body, title: title, type: kind, reason: reason,
|
|
998
|
+
basis: draft?.basis, report: draft?.report }
|
|
999
|
+
if (selected) { input.id = selected.meta.id; input.version = selected.meta.version }
|
|
1000
|
+
var entry = await api(endpoint, input)
|
|
1001
|
+
await read(entry.meta.id, entry.meta.version)
|
|
1002
|
+
await list()
|
|
1003
|
+
setMessage(T('research.saved'))
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function readBasis() {
|
|
1008
|
+
run(async function () {
|
|
1009
|
+
var ref = selected.meta.links.find(function (link) { return link.kind === 'derived' })
|
|
1010
|
+
var input = { action: 'read', id: ref.id, version: ref.version }
|
|
1011
|
+
var entry = await api(isKnowledge ? REPORTS : KNOWLEDGE, input)
|
|
1012
|
+
.catch(function () { return isKnowledge ? null : api(REPORTS, input).catch(function () { return null }) })
|
|
1013
|
+
setBasis(entry || { body: T('asset.missing') })
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function reference() { return { id: selected.meta.id, version: selected.meta.version } }
|
|
1018
|
+
|
|
494
1019
|
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
|
-
|
|
1020
|
+
h('div', { className: 'gr-row' },
|
|
1021
|
+
h('button', { className: 'gr-btn gr-btn-ghost', onClick: function () { props.onNavigate('home') } }, T('research.back')),
|
|
1022
|
+
h('h2', { className: 'gr-h2' }, T('asset.' + props.kind)),
|
|
1023
|
+
h('button', { className: 'gr-btn', disabled: busy, onClick: start }, T('asset.new'))),
|
|
1024
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
1025
|
+
h('p', { role: 'status' }, message),
|
|
1026
|
+
items.map(function (entry) {
|
|
1027
|
+
return h('button', { key: entry.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
1028
|
+
onClick: function () { run(function () { return read(entry.meta.id, entry.meta.version) }) } }, entry.meta.title)
|
|
1029
|
+
}),
|
|
1030
|
+
draft ? h('form', { className: 'gr-card', onSubmit: save },
|
|
1031
|
+
selected ? h('h3', null, selected.meta.title) : h('label', null, T('asset.title'),
|
|
1032
|
+
h('input', { className: 'gr-input', value: title, maxLength: 300, required: true, onChange: function (event) { setTitle(event.target.value) } })),
|
|
1033
|
+
isKnowledge && !selected ? h('label', null, T('asset.type'),
|
|
1034
|
+
h('select', { value: kind, onChange: function (event) { setKind(event.target.value) } },
|
|
1035
|
+
['concept', 'theory', 'claim', 'method'].map(function (type) { return h('option', { key: type, value: type }, T('type.' + type)) }))) : null,
|
|
1036
|
+
selected ? h('label', null, T('report.history'),
|
|
1037
|
+
h('select', { value: selected.meta.version, disabled: busy, onChange: function (event) {
|
|
1038
|
+
var version = Number(event.target.value)
|
|
1039
|
+
run(function () { return read(selected.meta.id, version) })
|
|
1040
|
+
} }, selected.versions.map(function (version) { return h('option', { key: version, value: version }, 'v' + version) }))) : null,
|
|
1041
|
+
h('label', null, T('asset.body'), h('textarea', { className: 'gr-textarea', value: body, required: true,
|
|
1042
|
+
onChange: function (event) { setBody(event.target.value) } })),
|
|
1043
|
+
selected?.meta.revisionReason ? h('p', { className: 'gr-meta' }, selected.meta.revisionReason) : null,
|
|
1044
|
+
isKnowledge && selected ? h('label', null, T('asset.reason'),
|
|
1045
|
+
h('textarea', { className: 'gr-textarea', value: reason, required: true, onChange: function (event) { setReason(event.target.value) } })) : null,
|
|
1046
|
+
h('button', { className: 'gr-btn', disabled: busy || (selected && selected.meta.version !== selected.versions[0]) }, T('asset.save'))) : null,
|
|
1047
|
+
selected ? h('div', { className: 'gr-row' },
|
|
1048
|
+
selected.meta.links?.some(function (link) { return link.kind === 'derived' })
|
|
1049
|
+
? h('button', { className: 'gr-btn', disabled: busy, onClick: readBasis }, T('asset.basis')) : null,
|
|
1050
|
+
isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1051
|
+
props.onNavigate('research', { title: selected.meta.title, origin: reference() })
|
|
1052
|
+
} }, T('asset.research')) : null,
|
|
1053
|
+
isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1054
|
+
props.onNavigate('works', { title: selected.meta.title, body: selected.body, basis: reference() })
|
|
1055
|
+
} }, T('asset.compose')) : null,
|
|
1056
|
+
!isKnowledge ? h('button', { className: 'gr-btn', onClick: function () {
|
|
1057
|
+
run(async function () { await navigator.clipboard.writeText(selected.body); setMessage(T('asset.copied')) })
|
|
1058
|
+
} }, T('asset.copy')) : null) : null,
|
|
1059
|
+
basis ? h('article', { className: 'gr-card' },
|
|
1060
|
+
basis.meta ? h('h3', null, basis.meta.title, ' · v', basis.meta.version) : null,
|
|
1061
|
+
h('pre', { className: 'gr-code' }, basis.body)) : null)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function ResearchPage(props) {
|
|
1065
|
+
var [reportDraft, setReportDraft] = useState(null)
|
|
1066
|
+
var [studies, setStudies] = useState([])
|
|
1067
|
+
var [selected, setSelected] = useState(null)
|
|
1068
|
+
var [question, setQuestion] = useState(props.seed?.title || '')
|
|
1069
|
+
var [body, setBody] = useState('')
|
|
1070
|
+
var [busy, setBusy] = useState(true)
|
|
1071
|
+
var [error, setError] = useState(null)
|
|
1072
|
+
var [saved, setSaved] = useState(false)
|
|
1073
|
+
|
|
1074
|
+
useEffect(function () {
|
|
1075
|
+
var active = true
|
|
1076
|
+
api(RESEARCH).then(function (data) {
|
|
1077
|
+
if (active) setStudies(data.studies)
|
|
1078
|
+
}, function (problem) {
|
|
1079
|
+
if (active) setError(problem)
|
|
1080
|
+
}).finally(function () {
|
|
1081
|
+
if (active) setBusy(false)
|
|
1082
|
+
})
|
|
1083
|
+
return function () { active = false }
|
|
1084
|
+
}, [])
|
|
1085
|
+
|
|
1086
|
+
function select(note) {
|
|
1087
|
+
setSelected(note)
|
|
1088
|
+
setReportDraft(null)
|
|
1089
|
+
setBody(note ? note.body : '')
|
|
1090
|
+
setQuestion('')
|
|
1091
|
+
setSaved(false)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function save(event) {
|
|
1095
|
+
event.preventDefault()
|
|
1096
|
+
if (busy || !body.trim() || (!selected && !question.trim())) return
|
|
1097
|
+
setBusy(true)
|
|
1098
|
+
setError(null)
|
|
1099
|
+
setSaved(false)
|
|
1100
|
+
var input = selected ? { id: selected.meta.id, version: selected.meta.version, body: body } : { question: question, body: body, origin: props.seed?.origin }
|
|
1101
|
+
api(RESEARCH, input).then(function (note) {
|
|
1102
|
+
select(note)
|
|
1103
|
+
setSaved(true)
|
|
1104
|
+
setStudies(function (items) {
|
|
1105
|
+
return [note].concat(items.filter(function (item) { return item.meta.id !== note.meta.id }))
|
|
1106
|
+
})
|
|
1107
|
+
}, setError).finally(function () { setBusy(false) })
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return h(React.Fragment, null,
|
|
1111
|
+
h('div', { className: 'gr-row' },
|
|
1112
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: props.onBack }, T('research.back')),
|
|
1113
|
+
studies.length ? h('button', { className: 'gr-btn', disabled: busy, onClick: function () { select(studies[0]) } }, T('research.continue')) : null,
|
|
1114
|
+
h('button', { className: 'gr-btn gr-btn-ghost', disabled: busy, onClick: function () { select(null) } }, T('research.new'))),
|
|
1115
|
+
error ? h('p', { role: 'alert' }, messageOf(error)) : null,
|
|
1116
|
+
h('form', { className: 'gr-card', onSubmit: save },
|
|
1117
|
+
selected ? h('h2', { className: 'gr-h2' }, selected.meta.title) :
|
|
1118
|
+
h('label', null, T('research.question'),
|
|
1119
|
+
h('input', { className: 'gr-input', value: question, required: true, maxLength: 300,
|
|
1120
|
+
onChange: function (event) { setQuestion(event.target.value) } })),
|
|
1121
|
+
h('label', null, T('research.notes'),
|
|
1122
|
+
h('textarea', { className: 'gr-textarea', value: body, required: true,
|
|
1123
|
+
onChange: function (event) { setBody(event.target.value); setSaved(false) } })),
|
|
1124
|
+
h('div', { className: 'gr-row' },
|
|
1125
|
+
h('button', { className: 'gr-btn', disabled: busy }, T('research.save')),
|
|
1126
|
+
h('span', { role: 'status' }, saved ? T('research.saved') : ''))),
|
|
1127
|
+
h(ModelSettings),
|
|
1128
|
+
h(ParserSettings),
|
|
1129
|
+
selected ? h(ResearchFindings, { key: 'findings:' + selected.meta.id, id: selected.meta.id, title: selected.meta.title, version: selected.meta.version, onDraft: setReportDraft }) : null,
|
|
1130
|
+
selected ? h(ResearchSources, { key: selected.meta.id, id: selected.meta.id }) : null,
|
|
1131
|
+
selected ? h(ResearchReports, { key: 'report:' + selected.meta.id, id: selected.meta.id, onNavigate: props.onNavigate, draft: reportDraft, onDiscard: function () { setReportDraft(null) } }) : null,
|
|
1132
|
+
studies.map(function (note) {
|
|
1133
|
+
return h('button', { key: note.meta.id, className: 'gr-btn gr-btn-ghost', disabled: busy,
|
|
1134
|
+
onClick: function () { select(note) } }, note.meta.title)
|
|
1135
|
+
}),
|
|
519
1136
|
)
|
|
520
1137
|
}
|
|
521
1138
|
|