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/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 [firstNote, setFirstNote] = useState(null)
253
- var [copied, setCopied] = useState(false)
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(CONFIG, { path: path }).then(function () {
281
- return waitForVault(path, 12)
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 saveFirst() {
323
- var body = draft.trim()
324
- if (body === '') return
325
- setBusy('capture')
413
+ function requestNotes(input, apply) {
414
+ if (busy) return
415
+ setBusy('read')
326
416
  setError(null)
327
- api(CAPTURE, { body: body }).then(function (note) {
328
- if (!mounted.current) return
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
- }).then(function () {
421
+ }).finally(function () {
337
422
  if (mounted.current) setBusy('')
338
423
  })
339
424
  }
340
425
 
341
- function copyPrompt(text) {
342
- copyText(text).then(function () {
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
- setCopied(true)
345
- setTimeout(function () { if (mounted.current) setCopied(false) }, 1600)
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
- : total > 0 && firstNote === null
380
- ? h(Overview, { status: status, onCopy: copyPrompt })
381
- : h(FirstNote, {
382
- status: status, draft: draft, busy: busy, note: firstNote,
383
- onDraft: setDraft, onSave: saveFirst, onCopy: copyPrompt,
384
- onOverview: function () { setFirstNote(null) },
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 FirstNote(props) {
448
- var status = props.status
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('div', { className: 'gr-meta' }, T('s1.connectedTo') + ' · ' + (status.vault || '')),
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, placeholder: T('s1.placeholder'),
464
- 'aria-label': T('s1.title'),
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 !== '' || props.draft.trim() === '', onClick: props.onSave },
472
- props.busy === 'capture' ? T('s1.saving') : T('s1.save')),
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-card' },
477
- h('div', { className: 'gr-h2' }, T('s1.another')),
478
- h('pre', { className: 'gr-code' }, T('s1.prompt')),
479
- h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', onClick: function () { props.onCopy(T('s1.prompt')) } }, T('s1.copy')),
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 Overview(props) {
485
- var status = props.status
486
- var counts = status.counts || {}
487
- var stats = ['inspiration', 'question', 'research', 'claim', 'evidence'].map(function (type) {
488
- return h('div', { className: 'gr-stat', key: type },
489
- h('b', null, String(counts[type] || 0)),
490
- h('span', null, T('s2.count.' + type)),
491
- )
492
- })
493
- var recent = status.recent || []
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-card' },
496
- h('div', { className: 'gr-row', style: { justifyContent: 'space-between' } },
497
- h('div', { className: 'gr-h2' }, T('s2.title')),
498
- h('div', { className: 'gr-meta' }, String(counts.total || 0) + ' ' + T('s2.total')),
499
- ),
500
- h('div', { className: 'gr-stats' }, stats),
501
- ),
502
- h('div', { className: 'gr-card' },
503
- h('div', { className: 'gr-h2' }, T('s2.recent')),
504
- recent.length === 0
505
- ? h('div', { className: 'gr-body' }, T('s2.empty'))
506
- : h('div', { className: 'gr-recent' }, recent.map(function (item) {
507
- return h('div', { className: 'gr-item', key: item.id + ':' + item.version },
508
- h('span', { className: 'gr-type' }, T('type.' + item.type)),
509
- h('span', { className: 'gr-itemtitle', title: item.title || '' }, item.title || item.id),
510
- h('span', { className: 'gr-meta' }, 'v' + item.version + (item.at ? ' · ' + when(item.at) : '')),
511
- )
512
- })),
513
- ),
514
- h('div', { className: 'gr-card' },
515
- h('div', { className: 'gr-h2' }, T('s2.continue')),
516
- h('pre', { className: 'gr-code' }, T('s2.prompt')),
517
- h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', onClick: function () { props.onCopy(T('s2.prompt')) } }, T('s1.copy')),
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