dsh-grove 0.1.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +34 -73
- package/README.md +35 -76
- package/client.js +1256 -0
- package/docs/DETAIL.en.md +2 -40
- package/docs/DETAIL.zh.md +2 -40
- package/docs/preview.svg +46 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +15 -3
- package/src/adapters/models/http.js +24 -0
- package/src/adapters/parsers/documents.js +31 -0
- package/src/adapters/storage/obsidian/backup.js +104 -0
- package/src/cli.js +11 -3
- package/src/modules/evidence.js +38 -0
- package/src/modules/inspirations.js +18 -0
- package/src/modules/knowledge.js +29 -0
- package/src/modules/model-research.js +27 -0
- package/src/modules/reports.js +57 -0
- package/src/modules/research.js +17 -0
- package/src/modules/version-reference.js +4 -0
- package/src/modules/works.js +27 -0
- package/src/plugin.js +33 -4
- package/src/proposals.js +1 -1
- package/src/server.js +412 -0
- package/src/vault.js +16 -1
package/client.js
ADDED
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grove client half: the left-sidebar panel and its cold-start page.
|
|
3
|
+
*
|
|
4
|
+
* Single file, plain JavaScript, no build step. Only `require('react')` plus browser APIs.
|
|
5
|
+
* All data comes from the host half's same-origin routes:
|
|
6
|
+
* GET /api/grove/status
|
|
7
|
+
* POST /api/grove/config
|
|
8
|
+
* POST /api/grove/capture
|
|
9
|
+
* Styles use `--dsw-alias-*` tokens only.
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: 'dsh-grove',
|
|
13
|
+
factory: function (require) {
|
|
14
|
+
var React = require('react')
|
|
15
|
+
var h = React.createElement
|
|
16
|
+
var useState = React.useState
|
|
17
|
+
var useEffect = React.useEffect
|
|
18
|
+
var useRef = React.useRef
|
|
19
|
+
|
|
20
|
+
var NS = 'grove'
|
|
21
|
+
var STATUS = '/api/grove/status'
|
|
22
|
+
var CONNECTION = '/api/grove/connection'
|
|
23
|
+
var CONFIG = '/api/grove/config'
|
|
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'
|
|
34
|
+
|
|
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': '历史版本',
|
|
99
|
+
panel: 'Grove',
|
|
100
|
+
menu: '知林',
|
|
101
|
+
subtitle: '把探索留在自己的 vault 里。',
|
|
102
|
+
'status.connected': '已连接',
|
|
103
|
+
'status.disconnected': '未连接',
|
|
104
|
+
'status.readonly': '只读',
|
|
105
|
+
'status.checking': '正在读取…',
|
|
106
|
+
's0.title': '先连接你的 Obsidian vault',
|
|
107
|
+
's0.body': 'Grove 只读写 vault 里的 Grove/ 文件夹,不读其他笔记。连接后,Harness 里的对话就能落成可回看、可修订的 Markdown。',
|
|
108
|
+
's0.step1': '连接 vault',
|
|
109
|
+
's0.step2': '写下第一条灵感',
|
|
110
|
+
's0.step3': '回到会话继续探索',
|
|
111
|
+
's0.pick': '选择文件夹',
|
|
112
|
+
's0.manual': '手动输入路径',
|
|
113
|
+
's0.placeholder': '/Users/你/Obsidian Vault',
|
|
114
|
+
's0.connect': '连接',
|
|
115
|
+
's0.cancel': '取消',
|
|
116
|
+
's0.hint': '路径会保存到当前 profile 的插件配置;之后可在插件页里修改。',
|
|
117
|
+
's0.stale': '原来的 vault 已不可用:{path}。请重新选择,或把它移回原位。',
|
|
118
|
+
's0.noVault': '这个文件夹里没有 .obsidian。如果它不是 vault 根目录,仍可继续连接。',
|
|
119
|
+
's1.change': '更换 vault',
|
|
120
|
+
'type.inspiration': '灵感',
|
|
121
|
+
'type.question': '问题',
|
|
122
|
+
'type.concept': '概念',
|
|
123
|
+
'type.theory': '理论',
|
|
124
|
+
'type.claim': '判断',
|
|
125
|
+
'type.evidence': '证据',
|
|
126
|
+
'type.method': '方法',
|
|
127
|
+
'type.output': '成果',
|
|
128
|
+
'type.exploration': '探索',
|
|
129
|
+
'type.research': '研究',
|
|
130
|
+
'foot.privacy': 'Grove 只读写 vault 的 Grove/ 文件夹,不读其他笔记。',
|
|
131
|
+
'foot.confirm': 'AI 起草的草稿要你在终端确认后才落盘。',
|
|
132
|
+
'foot.data': '数据是你的 Markdown,随时可以备份或迁移。',
|
|
133
|
+
'err.invalid-path': '路径无效,请填写绝对路径。',
|
|
134
|
+
'err.not-directory': '这个路径不是文件夹。',
|
|
135
|
+
'err.symlink': '这个路径是符号链接,请选择真实目录。',
|
|
136
|
+
'err.save-unavailable': '当前 profile 无法保存插件配置,请手动设置 GROVE_VAULT。',
|
|
137
|
+
'err.not-configured': '还没有连接 vault。',
|
|
138
|
+
'err.invalid-body': '内容不能为空。',
|
|
139
|
+
'err.too-large': '内容太长了,请控制在 128 KiB 以内。',
|
|
140
|
+
'err.network': '连不上面板服务,请重启 DSH 后重试。',
|
|
141
|
+
'err.timeout': '配置已提交,但还没生效。请重启 DSH 后再看。',
|
|
142
|
+
'err.internal': '操作失败,请重试。',
|
|
143
|
+
'err.retry': '重试',
|
|
144
|
+
'err.clipboard': '复制被拦截,请手动选择文本。',
|
|
145
|
+
'loading': '正在读取 vault…',
|
|
146
|
+
}
|
|
147
|
+
|
|
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',
|
|
212
|
+
panel: 'Grove',
|
|
213
|
+
menu: 'Grove',
|
|
214
|
+
subtitle: 'Keep what you explore in your own vault.',
|
|
215
|
+
'status.connected': 'Connected',
|
|
216
|
+
'status.disconnected': 'Not connected',
|
|
217
|
+
'status.readonly': 'Read only',
|
|
218
|
+
'status.checking': 'Reading…',
|
|
219
|
+
's0.title': 'Connect your Obsidian vault',
|
|
220
|
+
'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.',
|
|
221
|
+
's0.step1': 'Connect a vault',
|
|
222
|
+
's0.step2': 'Capture a first idea',
|
|
223
|
+
's0.step3': 'Go back to the conversation',
|
|
224
|
+
's0.pick': 'Choose folder',
|
|
225
|
+
's0.manual': 'Enter a path',
|
|
226
|
+
's0.placeholder': '/Users/you/Obsidian Vault',
|
|
227
|
+
's0.connect': 'Connect',
|
|
228
|
+
's0.cancel': 'Cancel',
|
|
229
|
+
's0.hint': 'The path is saved in this profile’s plugin configuration and can be changed later from the Plugins page.',
|
|
230
|
+
's0.stale': 'The previous vault is no longer usable: {path}. Choose it again, or move it back.',
|
|
231
|
+
's0.noVault': 'This folder has no .obsidian. You can still connect if it is a vault root.',
|
|
232
|
+
's1.change': 'Change vault',
|
|
233
|
+
'type.inspiration': 'Inspiration',
|
|
234
|
+
'type.question': 'Question',
|
|
235
|
+
'type.concept': 'Concept',
|
|
236
|
+
'type.theory': 'Theory',
|
|
237
|
+
'type.claim': 'Claim',
|
|
238
|
+
'type.evidence': 'Evidence',
|
|
239
|
+
'type.method': 'Method',
|
|
240
|
+
'type.output': 'Output',
|
|
241
|
+
'type.exploration': 'Exploration',
|
|
242
|
+
'type.research': 'Research',
|
|
243
|
+
'foot.privacy': 'Grove reads and writes only the vault’s Grove/ folder, never other notes.',
|
|
244
|
+
'foot.confirm': 'Content drafted by AI is saved only after you confirm it in the terminal.',
|
|
245
|
+
'foot.data': 'Your data is Markdown you can back up or move at any time.',
|
|
246
|
+
'err.invalid-path': 'That path is not valid. Use an absolute path.',
|
|
247
|
+
'err.not-directory': 'That path is not a folder.',
|
|
248
|
+
'err.symlink': 'That path is a symlink. Choose a real directory.',
|
|
249
|
+
'err.save-unavailable': 'This profile cannot save plugin configuration. Set GROVE_VAULT manually.',
|
|
250
|
+
'err.not-configured': 'No vault is connected yet.',
|
|
251
|
+
'err.invalid-body': 'Write something first.',
|
|
252
|
+
'err.too-large': 'That is too long. Keep it under 128 KiB.',
|
|
253
|
+
'err.network': 'The panel service is unreachable. Restart DSH and try again.',
|
|
254
|
+
'err.timeout': 'The configuration was submitted but has not taken effect. Restart DSH and check again.',
|
|
255
|
+
'err.internal': 'That did not work. Try again.',
|
|
256
|
+
'err.retry': 'Retry',
|
|
257
|
+
'err.clipboard': 'Copy was blocked. Select the text manually.',
|
|
258
|
+
'loading': 'Reading the vault…',
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var boundT = null
|
|
262
|
+
var services = { workspace: null }
|
|
263
|
+
|
|
264
|
+
function T(key, vars) {
|
|
265
|
+
if (boundT) {
|
|
266
|
+
try {
|
|
267
|
+
var resolved = boundT(key, vars)
|
|
268
|
+
if (resolved !== undefined && resolved !== null && resolved !== key) return resolved
|
|
269
|
+
} catch (error) { /* fall through to the bundled dictionary */ }
|
|
270
|
+
}
|
|
271
|
+
var value = ZH[key] !== undefined ? ZH[key] : EN[key]
|
|
272
|
+
if (value === undefined) return key
|
|
273
|
+
if (vars) {
|
|
274
|
+
value = value.replace(/\{(\w+)\}/g, function (match, name) {
|
|
275
|
+
return vars[name] === undefined ? match : String(vars[name])
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
return value
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function api(path, body) {
|
|
282
|
+
var request = body === undefined
|
|
283
|
+
? { headers: { accept: 'application/json' } }
|
|
284
|
+
: { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(body) }
|
|
285
|
+
return fetch(path, request).then(function (response) {
|
|
286
|
+
return response.json().catch(function () { return {} }).then(function (data) {
|
|
287
|
+
if (response.ok) return data
|
|
288
|
+
var error = new Error((data.error && data.error.message) || ('HTTP ' + response.status))
|
|
289
|
+
error.code = data.error && data.error.code
|
|
290
|
+
throw error
|
|
291
|
+
})
|
|
292
|
+
}, function () {
|
|
293
|
+
var error = new Error('network')
|
|
294
|
+
error.code = 'network'
|
|
295
|
+
throw error
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function messageOf(error) {
|
|
300
|
+
if (error && error.code && ZH['err.' + error.code] !== undefined) return T('err.' + error.code)
|
|
301
|
+
if (error && error.code === 'network') return T('err.network')
|
|
302
|
+
return (error && error.message) || T('err.internal')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function waitForVault(expected, tries) {
|
|
306
|
+
return api(STATUS).catch(function () { return null }).then(function (status) {
|
|
307
|
+
if (status && status.configured && status.usable && (expected === undefined || status.vault === expected)) return status
|
|
308
|
+
if (tries <= 0) {
|
|
309
|
+
var error = new Error('timeout')
|
|
310
|
+
error.code = 'timeout'
|
|
311
|
+
throw error
|
|
312
|
+
}
|
|
313
|
+
return new Promise(function (resolve) { setTimeout(resolve, 700) }).then(function () {
|
|
314
|
+
return waitForVault(expected, tries - 1)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ────────────────────────────────────────────────────── the page
|
|
320
|
+
function GrovePage() {
|
|
321
|
+
var [status, setStatus] = useState(null)
|
|
322
|
+
var [loading, setLoading] = useState(true)
|
|
323
|
+
var [error, setError] = useState(null)
|
|
324
|
+
var [busy, setBusy] = useState('')
|
|
325
|
+
var [manual, setManual] = useState(false)
|
|
326
|
+
var [draft, setDraft] = useState('')
|
|
327
|
+
var [pathDraft, setPathDraft] = useState('')
|
|
328
|
+
var [query, setQuery] = useState('')
|
|
329
|
+
var [notes, setNotes] = useState(null)
|
|
330
|
+
var [selected, setSelected] = useState(null)
|
|
331
|
+
var [saved, setSaved] = useState(false)
|
|
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
|
+
}
|
|
340
|
+
var mounted = useRef(true)
|
|
341
|
+
|
|
342
|
+
useEffect(function () {
|
|
343
|
+
mounted.current = true
|
|
344
|
+
load()
|
|
345
|
+
return function () { mounted.current = false }
|
|
346
|
+
}, [])
|
|
347
|
+
|
|
348
|
+
function load() {
|
|
349
|
+
setLoading(true)
|
|
350
|
+
return api(STATUS).then(function (next) {
|
|
351
|
+
if (!mounted.current) return
|
|
352
|
+
setStatus(next)
|
|
353
|
+
setError(null)
|
|
354
|
+
}, function (problem) {
|
|
355
|
+
if (!mounted.current) return
|
|
356
|
+
setError(problem)
|
|
357
|
+
}).then(function () {
|
|
358
|
+
if (mounted.current) setLoading(false)
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function connect(path) {
|
|
363
|
+
setBusy('connect')
|
|
364
|
+
setError(null)
|
|
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
|
+
})
|
|
369
|
+
}).then(function (next) {
|
|
370
|
+
if (!mounted.current) return
|
|
371
|
+
setStatus(next)
|
|
372
|
+
navigate('home')
|
|
373
|
+
setNotes(null)
|
|
374
|
+
setSelected(null)
|
|
375
|
+
setSaved(false)
|
|
376
|
+
setManual(false)
|
|
377
|
+
setChanging(false)
|
|
378
|
+
}, function (problem) {
|
|
379
|
+
if (!mounted.current) return
|
|
380
|
+
setError(problem)
|
|
381
|
+
}).then(function () {
|
|
382
|
+
if (mounted.current) setBusy('')
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function pickFolder() {
|
|
387
|
+
var workspace = services.workspace
|
|
388
|
+
if (!workspace || typeof workspace.pickDirectory !== 'function') {
|
|
389
|
+
setManual(true)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
setBusy('pick')
|
|
393
|
+
Promise.resolve(workspace.pickDirectory()).then(function (path) {
|
|
394
|
+
if (!path) {
|
|
395
|
+
if (mounted.current) setBusy('')
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
if (mounted.current) setBusy('')
|
|
399
|
+
connect(path)
|
|
400
|
+
}, function () {
|
|
401
|
+
if (!mounted.current) return
|
|
402
|
+
setBusy('')
|
|
403
|
+
setManual(true)
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function submitManual() {
|
|
408
|
+
var path = pathDraft.trim()
|
|
409
|
+
if (path === '') return
|
|
410
|
+
connect(path)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function requestNotes(input, apply) {
|
|
414
|
+
if (busy) return
|
|
415
|
+
setBusy('read')
|
|
416
|
+
setError(null)
|
|
417
|
+
return api(INSPIRATIONS, input).then(function (data) {
|
|
418
|
+
if (mounted.current) apply(data)
|
|
419
|
+
}, function (problem) {
|
|
420
|
+
if (mounted.current) setError(problem)
|
|
421
|
+
}).finally(function () {
|
|
422
|
+
if (mounted.current) setBusy('')
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
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 () {
|
|
433
|
+
if (!mounted.current) return
|
|
434
|
+
setDraft('')
|
|
435
|
+
setSaved(true)
|
|
436
|
+
setNotes(null)
|
|
437
|
+
setSelected(null)
|
|
438
|
+
}, function (problem) {
|
|
439
|
+
if (mounted.current) setError(problem)
|
|
440
|
+
}).finally(function () {
|
|
441
|
+
if (mounted.current) setBusy('')
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
var connected = !!(status && status.configured && status.usable)
|
|
446
|
+
var connectView = h(Connect, {
|
|
447
|
+
busy: busy, manual: manual, error: error, pathDraft: pathDraft,
|
|
448
|
+
stale: !changing && status && status.configured && !status.usable ? status.vault : null,
|
|
449
|
+
onPath: setPathDraft, onPick: pickFolder, onSubmit: submitManual,
|
|
450
|
+
onManual: function () { setManual(true) },
|
|
451
|
+
onManualCancel: function () { setManual(false) },
|
|
452
|
+
onCancel: changing || connected ? function () { setChanging(false); setManual(false) } : null,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
return h('div', { className: 'gr-root' },
|
|
456
|
+
h('style', null, CSS),
|
|
457
|
+
h('div', { className: 'gr-scroll' },
|
|
458
|
+
h('div', { className: 'gr-inner' },
|
|
459
|
+
h(Header, {
|
|
460
|
+
status: status, loading: loading, error: error,
|
|
461
|
+
onChange: connected ? function () {
|
|
462
|
+
setPathDraft(status.vault || '')
|
|
463
|
+
setManual(false)
|
|
464
|
+
setChanging(true)
|
|
465
|
+
} : null,
|
|
466
|
+
}),
|
|
467
|
+
error ? h(ErrorNote, { error: error, onRetry: load }) : null,
|
|
468
|
+
loading && status === null
|
|
469
|
+
? h('div', { className: 'gr-card' }, h('div', { className: 'gr-body' }, T('loading')))
|
|
470
|
+
: changing || !connected
|
|
471
|
+
? connectView
|
|
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
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
h(Footer, null),
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function Header(props) {
|
|
497
|
+
var status = props.status
|
|
498
|
+
var connected = !!(status && status.configured && status.usable)
|
|
499
|
+
var label = props.loading && !status ? T('status.checking') : connected ? T('status.connected') : T('status.disconnected')
|
|
500
|
+
return h('div', { className: 'gr-head' },
|
|
501
|
+
h('div', { className: 'gr-mark' }, h(GroveMark, { size: 26 })),
|
|
502
|
+
h('div', { className: 'gr-headtext' },
|
|
503
|
+
h('div', { className: 'gr-title' }, T('panel')),
|
|
504
|
+
h('div', { className: 'gr-sub' }, status && status.vault ? status.vault : T('subtitle')),
|
|
505
|
+
),
|
|
506
|
+
h('div', { className: 'gr-head-actions' },
|
|
507
|
+
props.onChange
|
|
508
|
+
? h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', onClick: props.onChange }, T('s1.change'))
|
|
509
|
+
: null,
|
|
510
|
+
h('span', { className: 'gr-pill', 'aria-live': 'polite' },
|
|
511
|
+
h('span', { className: 'gr-dot ' + (connected ? 'gr-dot-ok' : '') }),
|
|
512
|
+
label,
|
|
513
|
+
connected && status.writable === false ? h('span', null, ' · ' + T('status.readonly')) : null,
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function Connect(props) {
|
|
520
|
+
return h('div', { className: 'gr-card' },
|
|
521
|
+
props.stale ? h('div', { className: 'gr-err' }, T('s0.stale', { path: props.stale })) : null,
|
|
522
|
+
h('div', { className: 'gr-h2' }, T('s0.title')),
|
|
523
|
+
h('div', { className: 'gr-body' }, T('s0.body')),
|
|
524
|
+
h('div', { className: 'gr-steps' },
|
|
525
|
+
h('div', { className: 'gr-step' }, h('b', null, '1'), T('s0.step1')),
|
|
526
|
+
h('div', { className: 'gr-step' }, h('b', null, '2'), T('s0.step2')),
|
|
527
|
+
h('div', { className: 'gr-step' }, h('b', null, '3'), T('s0.step3')),
|
|
528
|
+
),
|
|
529
|
+
props.manual
|
|
530
|
+
? h('div', { className: 'gr-row' },
|
|
531
|
+
h('input', {
|
|
532
|
+
className: 'gr-input', value: props.pathDraft, spellCheck: false,
|
|
533
|
+
placeholder: T('s0.placeholder'), 'aria-label': T('s0.placeholder'),
|
|
534
|
+
onChange: function (event) { props.onPath(event.target.value) },
|
|
535
|
+
onKeyDown: function (event) { if (event.key === 'Enter') props.onSubmit() },
|
|
536
|
+
}),
|
|
537
|
+
h('button', { className: 'gr-btn gr-btn-sm', disabled: props.busy !== '', onClick: props.onSubmit }, T('s0.connect')),
|
|
538
|
+
h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', disabled: props.busy !== '', onClick: props.onManualCancel }, T('s0.cancel')),
|
|
539
|
+
)
|
|
540
|
+
: h('div', { className: 'gr-row' },
|
|
541
|
+
h('button', { className: 'gr-btn', disabled: props.busy !== '', onClick: props.onPick }, props.busy === 'pick' ? T('status.checking') : T('s0.pick')),
|
|
542
|
+
h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', disabled: props.busy !== '', onClick: props.onManual }, T('s0.manual')),
|
|
543
|
+
props.onCancel
|
|
544
|
+
? h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', disabled: props.busy !== '', onClick: props.onCancel }, T('s0.cancel'))
|
|
545
|
+
: null,
|
|
546
|
+
),
|
|
547
|
+
h('div', { className: 'gr-meta' }, T('s0.hint')),
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function InspirationHome(props) {
|
|
552
|
+
var selected = props.selected
|
|
553
|
+
return h(React.Fragment, null,
|
|
554
|
+
h('div', { className: 'gr-card' },
|
|
555
|
+
h('label', { className: 'gr-h2', htmlFor: 'gr-idea' }, T('idea.title')),
|
|
556
|
+
h('textarea', {
|
|
557
|
+
id: 'gr-idea', className: 'gr-textarea', value: props.draft,
|
|
558
|
+
placeholder: T('idea.placeholder'),
|
|
559
|
+
onChange: function (event) { props.onDraft(event.target.value) },
|
|
560
|
+
onKeyDown: function (event) {
|
|
561
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') props.onSave()
|
|
562
|
+
},
|
|
563
|
+
}),
|
|
564
|
+
h('div', { className: 'gr-row' },
|
|
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') : ''),
|
|
567
|
+
),
|
|
568
|
+
),
|
|
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,
|
|
596
|
+
),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
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
|
+
|
|
1019
|
+
return h(React.Fragment, null,
|
|
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
|
+
}),
|
|
1136
|
+
)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function ErrorNote(props) {
|
|
1140
|
+
return h('div', { className: 'gr-card', role: 'alert' },
|
|
1141
|
+
h('div', { className: 'gr-err' }, messageOf(props.error)),
|
|
1142
|
+
h('button', { className: 'gr-btn gr-btn-ghost gr-btn-sm', onClick: props.onRetry }, T('err.retry')),
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function Footer() {
|
|
1147
|
+
return h('div', { className: 'gr-foot' },
|
|
1148
|
+
h('div', null, T('foot.privacy')),
|
|
1149
|
+
h('div', null, T('foot.confirm')),
|
|
1150
|
+
h('div', null, T('foot.data')),
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function GroveMark(props) {
|
|
1155
|
+
var size = Number(props && props.size) || 36
|
|
1156
|
+
return h('svg', {
|
|
1157
|
+
xmlns: 'http://www.w3.org/2000/svg', width: size, height: size, viewBox: '0 0 36 36',
|
|
1158
|
+
'aria-hidden': true, focusable: 'false',
|
|
1159
|
+
},
|
|
1160
|
+
h('g', { transform: 'translate(18 18) scale(1.12) translate(-18.6 -20.9)' },
|
|
1161
|
+
h('path', { d: 'M16.9 30.4h2.2l-.65-12.3h-.9z', fill: '#1B3A2E' }),
|
|
1162
|
+
h('path', { d: 'M18.4 19c2.2-6.5 8.3-9.4 10.5-8.15-1 5.3-4.9 8.45-10.5 8.15Z', fill: '#2F7D5B' }),
|
|
1163
|
+
h('path', { d: 'M17.6 23c-2.2-5.1-6.5-7.3-8.5-6.3.7 4.1 4.3 6.3 8.5 6.3Z', fill: '#5FA37E' }),
|
|
1164
|
+
h('path', { d: 'M15.4 31.5h5.2', stroke: '#C98A3B', 'stroke-width': 2.2, 'stroke-linecap': 'round', fill: 'none' }),
|
|
1165
|
+
))
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function GroveIcon(props) {
|
|
1169
|
+
var size = Number(props && props.size) || 16
|
|
1170
|
+
return h(GroveMark, { size: size })
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
var CSS = [
|
|
1174
|
+
'.gr-root{height:100%;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:1.6;box-sizing:border-box;padding-top:min(var(--dsh-frame-top-clearance,0px),28px)}',
|
|
1175
|
+
'.gr-scroll{flex:1 1 auto;min-height:0;overflow:auto}',
|
|
1176
|
+
'.gr-inner{max-width:760px;margin:0 auto;padding:28px 24px 40px;display:flex;flex-direction:column;gap:18px;box-sizing:border-box}',
|
|
1177
|
+
'.gr-head{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}',
|
|
1178
|
+
'.gr-mark{flex:none;width:44px;height:44px;border-radius:12px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);display:grid;place-items:center}',
|
|
1179
|
+
'.gr-headtext{min-width:0;flex:1 1 220px}',
|
|
1180
|
+
'.gr-title{font-size:20px;font-weight:600;line-height:1.3}',
|
|
1181
|
+
'.gr-sub{color:var(--dsw-alias-label-secondary);font-size:13px;margin-top:2px;word-break:break-all}',
|
|
1182
|
+
'.gr-head-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex:none}',
|
|
1183
|
+
'.gr-pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:3px 10px;white-space:nowrap;background:var(--dsw-alias-bg-layer-1)}',
|
|
1184
|
+
'.gr-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-idle-primary)}',
|
|
1185
|
+
'.gr-dot-ok{background:var(--dsw-alias-state-success-primary)}',
|
|
1186
|
+
'.gr-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:12px;box-sizing:border-box}',
|
|
1187
|
+
'.gr-h2{font-size:15px;font-weight:600;line-height:1.4}',
|
|
1188
|
+
'.gr-body{color:var(--dsw-alias-label-secondary);font-size:13px}',
|
|
1189
|
+
'.gr-meta{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
1190
|
+
'.gr-btn{appearance:none;border:0;border-radius:8px;background:var(--dsw-alias-brand-primary);color:#fff;font:inherit;font-size:13px;font-weight:600;padding:8px 14px;cursor:pointer;display:inline-flex;align-items:center;gap:8px;align-self:flex-start}',
|
|
1191
|
+
'.gr-btn:hover{opacity:.92}',
|
|
1192
|
+
'.gr-btn:disabled{opacity:.5;cursor:default}',
|
|
1193
|
+
'.gr-btn-ghost{background:transparent;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2)}',
|
|
1194
|
+
'.gr-btn-sm{padding:5px 10px;font-size:12px;font-weight:500}',
|
|
1195
|
+
'.gr-input,.gr-textarea{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;padding:9px 11px;outline:none}',
|
|
1196
|
+
'.gr-textarea{min-height:96px;resize:vertical;line-height:1.6}',
|
|
1197
|
+
'.gr-input:focus-visible,.gr-textarea:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
1198
|
+
'.gr-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;min-width:0}',
|
|
1199
|
+
'.gr-err{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap}',
|
|
1200
|
+
'.gr-ok{color:var(--dsw-alias-state-success-primary)}',
|
|
1201
|
+
'.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}',
|
|
1202
|
+
'.gr-steps{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}',
|
|
1203
|
+
'.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)}',
|
|
1204
|
+
'.gr-step b{display:block;color:var(--dsw-alias-label-primary);font-size:12px;margin-bottom:2px}',
|
|
1205
|
+
'.gr-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:10px}',
|
|
1206
|
+
'.gr-stat{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-base);min-width:0}',
|
|
1207
|
+
'.gr-stat b{display:block;font-size:20px;font-weight:600;line-height:1.2}',
|
|
1208
|
+
'.gr-stat span{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
1209
|
+
'.gr-recent{display:flex;flex-direction:column;min-width:0}',
|
|
1210
|
+
'.gr-item{display:flex;align-items:baseline;gap:10px;padding:9px 0;border-bottom:1px solid var(--dsw-alias-border-l1);min-width:0}',
|
|
1211
|
+
'.gr-item:last-child{border-bottom:0}',
|
|
1212
|
+
'.gr-type{flex:none;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:1px 8px}',
|
|
1213
|
+
'.gr-itemtitle{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
1214
|
+
'.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}',
|
|
1215
|
+
'.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}',
|
|
1216
|
+
'@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}}',
|
|
1217
|
+
'@media (prefers-reduced-motion:reduce){.gr-btn{transition:none}}',
|
|
1218
|
+
].join('\n')
|
|
1219
|
+
|
|
1220
|
+
// ────────────────────────────────────────────────────── registration
|
|
1221
|
+
function apply(ctx) {
|
|
1222
|
+
var locale = ctx.locale
|
|
1223
|
+
if (locale) {
|
|
1224
|
+
try {
|
|
1225
|
+
if (typeof ctx.effect === 'function') {
|
|
1226
|
+
ctx.effect(function () { return locale.register(NS, { zh: ZH, en: EN }) }, 'grove: dictionaries')
|
|
1227
|
+
}
|
|
1228
|
+
if (typeof locale.bind === 'function') boundT = locale.bind(NS)
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
console.warn('[grove] locale registration failed', error)
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
services.workspace = ctx.get('uiWorkspace')
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
services.workspace = null
|
|
1237
|
+
}
|
|
1238
|
+
var slots = ctx.slots
|
|
1239
|
+
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
1240
|
+
console.warn('[grove] slots service unavailable; the panel is not mounted')
|
|
1241
|
+
return
|
|
1242
|
+
}
|
|
1243
|
+
slots.inject('main', function () {
|
|
1244
|
+
return slots.register({ name: 'main', key: 'grove', locale: NS }, GrovePage)
|
|
1245
|
+
})
|
|
1246
|
+
slots.inject('sidebar.panellist', function () {
|
|
1247
|
+
return slots.register({
|
|
1248
|
+
name: 'sidebar.panellist', id: 'grove', order: 40,
|
|
1249
|
+
label: function () { return T('menu') }, locale: NS,
|
|
1250
|
+
}, GroveIcon)
|
|
1251
|
+
})
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return { inject: ['slots', 'locale'], apply: apply }
|
|
1255
|
+
},
|
|
1256
|
+
})
|