dsh-grove 0.2.0 → 0.5.1

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