plugin-document-parser 1.0.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.
Files changed (47) hide show
  1. package/client.d.ts +2 -0
  2. package/client.js +1 -0
  3. package/dist/client/01b8a5798a872638.js +10 -0
  4. package/dist/client/022be20abc96fdb4.js +10 -0
  5. package/dist/client/12e97e7a84d900e0.js +10 -0
  6. package/dist/client/index.js +10 -0
  7. package/dist/externalVersion.js +20 -0
  8. package/dist/index.js +48 -0
  9. package/dist/locale/en-US.json +54 -0
  10. package/dist/locale/vi-VN.json +54 -0
  11. package/dist/node_modules/form-data/License +19 -0
  12. package/dist/node_modules/form-data/index.d.ts +62 -0
  13. package/dist/node_modules/form-data/lib/browser.js +4 -0
  14. package/dist/node_modules/form-data/lib/form_data.js +14 -0
  15. package/dist/node_modules/form-data/lib/populate.js +10 -0
  16. package/dist/node_modules/form-data/package.json +1 -0
  17. package/dist/server/collections/doc-parser-providers.js +137 -0
  18. package/dist/server/collections/doc-parser-settings.js +85 -0
  19. package/dist/server/index.js +51 -0
  20. package/dist/server/plugin.js +181 -0
  21. package/dist/server/resource/docParserProviders.js +91 -0
  22. package/dist/server/services/builtin-ai-handler.js +63 -0
  23. package/dist/server/services/external-ocr-client.js +189 -0
  24. package/dist/server/services/internal-parser-registry.js +82 -0
  25. package/dist/server/services/parse-router.js +273 -0
  26. package/package.json +33 -0
  27. package/server.d.ts +2 -0
  28. package/server.js +1 -0
  29. package/src/client/components/GlobalSettings.tsx +151 -0
  30. package/src/client/components/ProviderForm.tsx +266 -0
  31. package/src/client/components/ProviderList.tsx +193 -0
  32. package/src/client/components/SettingsPage.tsx +43 -0
  33. package/src/client/index.tsx +2 -0
  34. package/src/client/locale.ts +12 -0
  35. package/src/client/plugin.tsx +34 -0
  36. package/src/index.ts +2 -0
  37. package/src/locale/en-US.json +54 -0
  38. package/src/locale/vi-VN.json +54 -0
  39. package/src/server/collections/doc-parser-providers.ts +107 -0
  40. package/src/server/collections/doc-parser-settings.ts +59 -0
  41. package/src/server/index.ts +10 -0
  42. package/src/server/plugin.ts +172 -0
  43. package/src/server/resource/docParserProviders.ts +72 -0
  44. package/src/server/services/builtin-ai-handler.ts +49 -0
  45. package/src/server/services/external-ocr-client.ts +233 -0
  46. package/src/server/services/internal-parser-registry.ts +126 -0
  47. package/src/server/services/parse-router.ts +357 -0
@@ -0,0 +1,151 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Card, Form, Radio, Switch, Select, Button, message, Spin, Divider, Alert, Space, Typography } from 'antd';
3
+ import { SaveOutlined } from '@ant-design/icons';
4
+ import { useAPIClient } from '@nocobase/client';
5
+ import { useDocParserTranslation } from '../locale';
6
+
7
+ const { Text } = Typography;
8
+
9
+ type Settings = {
10
+ id?: number;
11
+ mode: 'default' | 'internal' | 'external';
12
+ activeProviderId?: number | null;
13
+ fallbackToDefault: boolean;
14
+ imagePassThrough: boolean;
15
+ includedExtnames: string[];
16
+ useDocpixie: boolean;
17
+ };
18
+
19
+ type Provider = {
20
+ id: number;
21
+ title: string;
22
+ enabled: boolean;
23
+ };
24
+
25
+ export const GlobalSettings: React.FC = () => {
26
+ const { t } = useDocParserTranslation();
27
+ const api = useAPIClient();
28
+ const [form] = Form.useForm<Settings>();
29
+ const [loading, setLoading] = useState(true);
30
+ const [saving, setSaving] = useState(false);
31
+ const [providers, setProviders] = useState<Provider[]>([]);
32
+ const [mode, setMode] = useState<'default' | 'internal' | 'external'>('default');
33
+
34
+ useEffect(() => {
35
+ Promise.all([
36
+ api.request({ url: 'docParserSettings:get' }),
37
+ api.request({ url: 'docParserProviders:list', params: { pageSize: 200 } }),
38
+ ]).then(([settingsRes, providersRes]) => {
39
+ const settings: Settings = settingsRes?.data?.data ?? {
40
+ mode: 'default',
41
+ fallbackToDefault: true,
42
+ imagePassThrough: true,
43
+ includedExtnames: [],
44
+ };
45
+ form.setFieldsValue(settings);
46
+ setMode(settings.mode);
47
+
48
+ const list: Provider[] = providersRes?.data?.data ?? [];
49
+ setProviders(list);
50
+ }).finally(() => setLoading(false));
51
+ }, []);
52
+
53
+ const handleSave = async () => {
54
+ const values = await form.validateFields();
55
+ setSaving(true);
56
+ try {
57
+ await api.request({ url: 'docParserSettings:save', method: 'POST', data: values });
58
+ message.success(t('Settings saved'));
59
+ } catch (err: any) {
60
+ message.error(err?.message ?? 'Save failed');
61
+ } finally {
62
+ setSaving(false);
63
+ }
64
+ };
65
+
66
+ if (loading) return <Spin style={{ display: 'block', margin: '40px auto' }} />;
67
+
68
+ const modeDescriptions: Record<string, string> = {
69
+ default: t('mode_default_desc'),
70
+ internal: t('mode_internal_desc'),
71
+ external: t('mode_external_desc'),
72
+ };
73
+
74
+ return (
75
+ <Card bordered={false}>
76
+ <Form form={form} layout="vertical" onValuesChange={(changed) => {
77
+ if (changed.mode) setMode(changed.mode);
78
+ }}>
79
+
80
+ <Form.Item name="mode" label={t('Processing Mode')}>
81
+ <Radio.Group style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
82
+ <Radio value="default">
83
+ <Space direction="vertical" size={0}>
84
+ <Text strong>{t('Default (plugin-ai built-in)')}</Text>
85
+ <Text type="secondary" style={{ fontSize: 12 }}>{t('mode_default_desc')}</Text>
86
+ </Space>
87
+ </Radio>
88
+ <Radio value="internal">
89
+ <Space direction="vertical" size={0}>
90
+ <Text strong>{t('Internal (built-in document loaders)')}</Text>
91
+ <Text type="secondary" style={{ fontSize: 12 }}>{t('mode_internal_desc')}</Text>
92
+ </Space>
93
+ </Radio>
94
+ <Radio value="external">
95
+ <Space direction="vertical" size={0}>
96
+ <Text strong>{t('External (OCR API provider)')}</Text>
97
+ <Text type="secondary" style={{ fontSize: 12 }}>{t('mode_external_desc')}</Text>
98
+ </Space>
99
+ </Radio>
100
+ </Radio.Group>
101
+ </Form.Item>
102
+
103
+ {mode === 'external' && (
104
+ <Form.Item
105
+ name="activeProviderId"
106
+ label={t('Active Provider')}
107
+ rules={[{ required: true, message: t('Please select a provider') }]}
108
+ >
109
+ <Select
110
+ placeholder={t('Please select a provider')}
111
+ options={providers.filter((p) => p.enabled).map((p) => ({
112
+ label: p.title,
113
+ value: p.id,
114
+ }))}
115
+ style={{ maxWidth: 400 }}
116
+ />
117
+ </Form.Item>
118
+ )}
119
+
120
+ <Divider />
121
+
122
+ <Form.Item name="fallbackToDefault" valuePropName="checked" label={t('Fallback to default on error')}>
123
+ <Switch />
124
+ </Form.Item>
125
+
126
+ <Form.Item name="imagePassThrough" valuePropName="checked" label={t('Pass images through to default')}>
127
+ <Switch />
128
+ </Form.Item>
129
+
130
+ <Divider />
131
+
132
+ <Form.Item
133
+ name="useDocpixie"
134
+ valuePropName="checked"
135
+ label={t('Index with DocPixie (when available)')}
136
+ help={<Text type="secondary">{t('docpixie_mode_desc')}</Text>}
137
+ >
138
+ <Switch />
139
+ </Form.Item>
140
+
141
+ <Divider />
142
+
143
+ <Form.Item>
144
+ <Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>
145
+ {t('Settings saved').replace('saved', 'Save')}
146
+ </Button>
147
+ </Form.Item>
148
+ </Form>
149
+ </Card>
150
+ );
151
+ };
@@ -0,0 +1,266 @@
1
+ import React, { useEffect } from 'react';
2
+ import {
3
+ Form, Input, InputNumber, Select, Switch, Button, Space, Divider,
4
+ Typography, Collapse, Row, Col,
5
+ } from 'antd';
6
+ import { useDocParserTranslation } from '../locale';
7
+
8
+ const { Text } = Typography;
9
+ const { Panel } = Collapse;
10
+
11
+ export type ProviderFormValues = {
12
+ title: string;
13
+ enabled: boolean;
14
+ apiEndpoint: string;
15
+ authType: 'bearer' | 'api-key-header' | 'basic' | 'custom-headers' | 'none';
16
+ apiKey?: string;
17
+ authConfig?: {
18
+ headerName?: string;
19
+ username?: string;
20
+ password?: string;
21
+ customHeaders?: string; // JSON string in UI, parsed on save
22
+ };
23
+ requestFormat: 'multipart' | 'json-base64' | 'url';
24
+ requestConfig?: {
25
+ fileFieldName?: string;
26
+ filenameFieldName?: string;
27
+ mimetypeFieldName?: string;
28
+ extraFields?: string; // JSON string
29
+ base64FieldPath?: string;
30
+ filenameFieldPath?: string;
31
+ mimetypeFieldPath?: string;
32
+ extraBody?: string; // JSON string
33
+ urlFieldPath?: string;
34
+ };
35
+ responseTextPath?: string;
36
+ timeout?: number;
37
+ supportedMimetypes?: string[];
38
+ };
39
+
40
+ type Props = {
41
+ initialValues?: Partial<ProviderFormValues>;
42
+ onSubmit: (values: ProviderFormValues) => Promise<void>;
43
+ onCancel: () => void;
44
+ submitting?: boolean;
45
+ };
46
+
47
+ export const ProviderForm: React.FC<Props> = ({ initialValues, onSubmit, onCancel, submitting }) => {
48
+ const { t } = useDocParserTranslation();
49
+ const [form] = Form.useForm<ProviderFormValues>();
50
+ const authType = Form.useWatch('authType', form);
51
+ const requestFormat = Form.useWatch('requestFormat', form);
52
+
53
+ useEffect(() => {
54
+ if (initialValues) {
55
+ form.setFieldsValue(initialValues);
56
+ }
57
+ }, [initialValues]);
58
+
59
+ const handleSubmit = async () => {
60
+ const values = await form.validateFields();
61
+ await onSubmit(values);
62
+ };
63
+
64
+ return (
65
+ <Form form={form} layout="vertical" initialValues={{
66
+ enabled: true,
67
+ authType: 'bearer',
68
+ requestFormat: 'multipart',
69
+ responseTextPath: 'text',
70
+ timeout: 60000,
71
+ requestConfig: { fileFieldName: 'file' },
72
+ ...initialValues,
73
+ }}>
74
+
75
+ {/* ── Basic ──────────────────────────────────────────── */}
76
+ <Row gutter={16}>
77
+ <Col span={18}>
78
+ <Form.Item name="title" label={t('Provider Title')} rules={[{ required: true }]}>
79
+ <Input />
80
+ </Form.Item>
81
+ </Col>
82
+ <Col span={6}>
83
+ <Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
84
+ <Switch />
85
+ </Form.Item>
86
+ </Col>
87
+ </Row>
88
+
89
+ <Form.Item name="apiEndpoint" label={t('API Endpoint')} rules={[{ required: true }]}>
90
+ <Input placeholder="https://api.ocr-provider.com/v1/parse" />
91
+ </Form.Item>
92
+
93
+ {/* ── Auth ───────────────────────────────────────────── */}
94
+ <Divider orientation="left" plain>{t('Auth Type')}</Divider>
95
+
96
+ <Form.Item name="authType" label={t('Auth Type')}>
97
+ <Select options={[
98
+ { label: 'None', value: 'none' },
99
+ { label: 'Bearer Token', value: 'bearer' },
100
+ { label: 'API Key Header', value: 'api-key-header' },
101
+ { label: 'Basic Auth', value: 'basic' },
102
+ { label: 'Custom Headers', value: 'custom-headers' },
103
+ ]} style={{ maxWidth: 260 }} />
104
+ </Form.Item>
105
+
106
+ {(authType === 'bearer' || authType === 'api-key-header') && (
107
+ <Form.Item name="apiKey" label={t('API Key')}>
108
+ <Input.Password />
109
+ </Form.Item>
110
+ )}
111
+
112
+ {authType === 'api-key-header' && (
113
+ <Form.Item name={['authConfig', 'headerName']} label={t('Header Name')}>
114
+ <Input placeholder="X-Api-Key" style={{ maxWidth: 300 }} />
115
+ </Form.Item>
116
+ )}
117
+
118
+ {authType === 'basic' && (
119
+ <Row gutter={16}>
120
+ <Col span={12}>
121
+ <Form.Item name={['authConfig', 'username']} label={t('Username')}>
122
+ <Input />
123
+ </Form.Item>
124
+ </Col>
125
+ <Col span={12}>
126
+ <Form.Item name={['authConfig', 'password']} label={t('Password')}>
127
+ <Input.Password />
128
+ </Form.Item>
129
+ </Col>
130
+ </Row>
131
+ )}
132
+
133
+ {authType === 'custom-headers' && (
134
+ <Form.Item
135
+ name={['authConfig', 'customHeaders']}
136
+ label={t('Custom Headers')}
137
+ help={<Text type="secondary">JSON: {'{"X-Tenant": "abc", "X-Secret": "xxx"}'}</Text>}
138
+ >
139
+ <Input.TextArea rows={3} placeholder='{"X-Tenant": "abc"}' />
140
+ </Form.Item>
141
+ )}
142
+
143
+ {/* ── Request format ─────────────────────────────────── */}
144
+ <Divider orientation="left" plain>{t('Request Format')}</Divider>
145
+
146
+ <Form.Item name="requestFormat" label={t('Request Format')}>
147
+ <Select options={[
148
+ { label: t('Multipart Form Data'), value: 'multipart' },
149
+ { label: t('JSON Base64'), value: 'json-base64' },
150
+ { label: 'URL (provider fetches)', value: 'url' },
151
+ ]} style={{ maxWidth: 260 }} />
152
+ </Form.Item>
153
+
154
+ {requestFormat === 'multipart' && (
155
+ <Row gutter={16}>
156
+ <Col span={8}>
157
+ <Form.Item name={['requestConfig', 'fileFieldName']} label={t('Form Field Name')}>
158
+ <Input placeholder="file" />
159
+ </Form.Item>
160
+ </Col>
161
+ <Col span={8}>
162
+ <Form.Item name={['requestConfig', 'filenameFieldName']} label={t('Filename Field Path')}>
163
+ <Input placeholder="(optional)" />
164
+ </Form.Item>
165
+ </Col>
166
+ <Col span={8}>
167
+ <Form.Item name={['requestConfig', 'mimetypeFieldName']} label={t('Mimetype Field Path')}>
168
+ <Input placeholder="(optional)" />
169
+ </Form.Item>
170
+ </Col>
171
+ </Row>
172
+ )}
173
+
174
+ {requestFormat === 'json-base64' && (
175
+ <Row gutter={16}>
176
+ <Col span={8}>
177
+ <Form.Item name={['requestConfig', 'base64FieldPath']} label={t('Base64 Field Path')}>
178
+ <Input placeholder="file" />
179
+ </Form.Item>
180
+ </Col>
181
+ <Col span={8}>
182
+ <Form.Item name={['requestConfig', 'filenameFieldPath']} label={t('Filename Field Path')}>
183
+ <Input placeholder="filename" />
184
+ </Form.Item>
185
+ </Col>
186
+ <Col span={8}>
187
+ <Form.Item name={['requestConfig', 'mimetypeFieldPath']} label={t('Mimetype Field Path')}>
188
+ <Input placeholder="mimetype" />
189
+ </Form.Item>
190
+ </Col>
191
+ </Row>
192
+ )}
193
+
194
+ {requestFormat === 'url' && (
195
+ <Form.Item name={['requestConfig', 'urlFieldPath']} label={t('Base64 Field Path').replace('Base64', 'URL')}>
196
+ <Input placeholder="url" style={{ maxWidth: 300 }} />
197
+ </Form.Item>
198
+ )}
199
+
200
+ <Collapse ghost>
201
+ <Panel header={t('Extra Request Body')} key="extra">
202
+ <Form.Item
203
+ name={['requestConfig', requestFormat === 'multipart' ? 'extraFields' : 'extraBody']}
204
+ help={<Text type="secondary">JSON object — merged into request body/fields</Text>}
205
+ >
206
+ <Input.TextArea rows={4} placeholder='{"lang": "vie+eng"}' />
207
+ </Form.Item>
208
+ </Panel>
209
+ </Collapse>
210
+
211
+ {/* ── Response ───────────────────────────────────────── */}
212
+ <Divider orientation="left" plain>Response</Divider>
213
+
214
+ <Row gutter={16}>
215
+ <Col span={16}>
216
+ <Form.Item
217
+ name="responseTextPath"
218
+ label={t('Response Text Path')}
219
+ help={<Text type="secondary">Dot-path, e.g. "data.text" or "result.pages.0.content"</Text>}
220
+ >
221
+ <Input placeholder="text" />
222
+ </Form.Item>
223
+ </Col>
224
+ <Col span={8}>
225
+ <Form.Item name="timeout" label={t('Timeout (ms)')}>
226
+ <InputNumber min={1000} step={5000} style={{ width: '100%' }} />
227
+ </Form.Item>
228
+ </Col>
229
+ </Row>
230
+
231
+ {/* ── Scope ──────────────────────────────────────────── */}
232
+ <Divider orientation="left" plain>Scope</Divider>
233
+
234
+ <Form.Item
235
+ name="supportedMimetypes"
236
+ label={t('Supported MIME Types')}
237
+ help={t('Leave empty to handle all non-image types')}
238
+ >
239
+ <Select
240
+ mode="tags"
241
+ tokenSeparators={[',']}
242
+ placeholder="application/pdf, application/vnd.openxmlformats..."
243
+ style={{ width: '100%' }}
244
+ options={[
245
+ { label: 'PDF', value: 'application/pdf' },
246
+ { label: 'DOCX', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
247
+ { label: 'XLSX', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
248
+ { label: 'PPTX', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
249
+ { label: 'Plain text', value: 'text/plain' },
250
+ { label: 'HTML', value: 'text/html' },
251
+ ]}
252
+ />
253
+ </Form.Item>
254
+
255
+ {/* ── Actions ────────────────────────────────────────── */}
256
+ <Form.Item style={{ marginBottom: 0, marginTop: 24 }}>
257
+ <Space>
258
+ <Button type="primary" onClick={handleSubmit} loading={submitting}>
259
+ Save
260
+ </Button>
261
+ <Button onClick={onCancel}>Cancel</Button>
262
+ </Space>
263
+ </Form.Item>
264
+ </Form>
265
+ );
266
+ };
@@ -0,0 +1,193 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Button, Table, Tag, Space, Popconfirm, Modal, message, Tooltip, Badge,
4
+ } from 'antd';
5
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
6
+ import { useAPIClient } from '@nocobase/client';
7
+ import { ProviderForm, ProviderFormValues } from './ProviderForm';
8
+ import { useDocParserTranslation } from '../locale';
9
+
10
+ type Provider = ProviderFormValues & { id: number };
11
+
12
+ export const ProviderList: React.FC = () => {
13
+ const { t } = useDocParserTranslation();
14
+ const api = useAPIClient();
15
+ const [providers, setProviders] = useState<Provider[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [modalOpen, setModalOpen] = useState(false);
18
+ const [editing, setEditing] = useState<Provider | null>(null);
19
+ const [submitting, setSubmitting] = useState(false);
20
+ const [testingId, setTestingId] = useState<number | null>(null);
21
+
22
+ const load = async () => {
23
+ setLoading(true);
24
+ try {
25
+ const res = await api.request({ url: 'docParserProviders:list', params: { pageSize: 200 } });
26
+ setProviders(res?.data?.data ?? []);
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ useEffect(() => { load(); }, []);
33
+
34
+ const openCreate = () => {
35
+ setEditing(null);
36
+ setModalOpen(true);
37
+ };
38
+
39
+ const openEdit = (record: Provider) => {
40
+ setEditing(record);
41
+ setModalOpen(true);
42
+ };
43
+
44
+ const handleDelete = async (id: number) => {
45
+ await api.request({ url: `docParserProviders:destroy`, method: 'DELETE', params: { filterByTk: id } });
46
+ message.success(t('Provider deleted'));
47
+ load();
48
+ };
49
+
50
+ const handleSubmit = async (values: ProviderFormValues) => {
51
+ setSubmitting(true);
52
+ try {
53
+ if (editing) {
54
+ await api.request({
55
+ url: 'docParserProviders:update',
56
+ method: 'POST',
57
+ params: { filterByTk: editing.id },
58
+ data: values,
59
+ });
60
+ } else {
61
+ await api.request({ url: 'docParserProviders:create', method: 'POST', data: values });
62
+ }
63
+ message.success(t('Provider saved'));
64
+ setModalOpen(false);
65
+ load();
66
+ } finally {
67
+ setSubmitting(false);
68
+ }
69
+ };
70
+
71
+ const handleTest = async (record: Provider) => {
72
+ setTestingId(record.id);
73
+ try {
74
+ const res = await api.request({
75
+ url: 'docParserProviders:testConnection',
76
+ params: { filterByTk: record.id },
77
+ });
78
+ const result = res?.data?.data;
79
+ if (result?.ok) {
80
+ message.success(`${t('Connection successful')}${result.status ? ` (HTTP ${result.status})` : ''}`);
81
+ } else {
82
+ message.error(`${t('Connection failed')}: ${result?.message ?? 'Unknown error'}`);
83
+ }
84
+ } catch (err: any) {
85
+ message.error(`${t('Connection failed')}: ${err?.message}`);
86
+ } finally {
87
+ setTestingId(null);
88
+ }
89
+ };
90
+
91
+ const columns = [
92
+ {
93
+ title: t('Provider Title'),
94
+ dataIndex: 'title',
95
+ key: 'title',
96
+ render: (title: string, record: Provider) => (
97
+ <Space>
98
+ <Badge status={record.enabled ? 'success' : 'default'} />
99
+ {title}
100
+ </Space>
101
+ ),
102
+ },
103
+ {
104
+ title: t('API Endpoint'),
105
+ dataIndex: 'apiEndpoint',
106
+ key: 'apiEndpoint',
107
+ ellipsis: true,
108
+ },
109
+ {
110
+ title: t('Auth Type'),
111
+ dataIndex: 'authType',
112
+ key: 'authType',
113
+ render: (v: string) => <Tag>{v}</Tag>,
114
+ },
115
+ {
116
+ title: t('Request Format'),
117
+ dataIndex: 'requestFormat',
118
+ key: 'requestFormat',
119
+ render: (v: string) => <Tag color="blue">{v}</Tag>,
120
+ },
121
+ {
122
+ title: t('Enabled'),
123
+ dataIndex: 'enabled',
124
+ key: 'enabled',
125
+ render: (v: boolean) =>
126
+ v ? <CheckCircleOutlined style={{ color: '#52c41a' }} /> : <CloseCircleOutlined style={{ color: '#ccc' }} />,
127
+ },
128
+ {
129
+ title: 'Actions',
130
+ key: 'actions',
131
+ render: (_: any, record: Provider) => (
132
+ <Space>
133
+ <Tooltip title={t('Test Connection')}>
134
+ <Button
135
+ size="small"
136
+ icon={<ApiOutlined />}
137
+ loading={testingId === record.id}
138
+ onClick={() => handleTest(record)}
139
+ />
140
+ </Tooltip>
141
+ <Tooltip title={t('Edit Provider')}>
142
+ <Button size="small" icon={<EditOutlined />} onClick={() => openEdit(record)} />
143
+ </Tooltip>
144
+ <Popconfirm
145
+ title={t('Delete Provider')}
146
+ description="Are you sure you want to delete this provider?"
147
+ onConfirm={() => handleDelete(record.id)}
148
+ >
149
+ <Tooltip title={t('Delete Provider')}>
150
+ <Button size="small" danger icon={<DeleteOutlined />} />
151
+ </Tooltip>
152
+ </Popconfirm>
153
+ </Space>
154
+ ),
155
+ },
156
+ ];
157
+
158
+ return (
159
+ <>
160
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
161
+ <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
162
+ {t('Add Provider')}
163
+ </Button>
164
+ </div>
165
+
166
+ <Table
167
+ rowKey="id"
168
+ dataSource={providers}
169
+ columns={columns}
170
+ loading={loading}
171
+ pagination={false}
172
+ locale={{ emptyText: t('No providers configured') }}
173
+ size="middle"
174
+ />
175
+
176
+ <Modal
177
+ open={modalOpen}
178
+ title={editing ? t('Edit Provider') : t('Add Provider')}
179
+ onCancel={() => setModalOpen(false)}
180
+ footer={null}
181
+ width={720}
182
+ destroyOnClose
183
+ >
184
+ <ProviderForm
185
+ initialValues={editing ?? undefined}
186
+ onSubmit={handleSubmit}
187
+ onCancel={() => setModalOpen(false)}
188
+ submitting={submitting}
189
+ />
190
+ </Modal>
191
+ </>
192
+ );
193
+ };
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Tabs } from 'antd';
3
+ import { SettingOutlined, ApiOutlined } from '@ant-design/icons';
4
+ import { GlobalSettings } from './GlobalSettings';
5
+ import { ProviderList } from './ProviderList';
6
+ import { useDocParserTranslation } from '../locale';
7
+
8
+ export const SettingsPage: React.FC = () => {
9
+ const { t } = useDocParserTranslation();
10
+
11
+ const items = [
12
+ {
13
+ key: 'global',
14
+ label: (
15
+ <span>
16
+ <SettingOutlined />
17
+ {t('Global Settings')}
18
+ </span>
19
+ ),
20
+ children: <GlobalSettings />,
21
+ },
22
+ {
23
+ key: 'providers',
24
+ label: (
25
+ <span>
26
+ <ApiOutlined />
27
+ {t('OCR Providers')}
28
+ </span>
29
+ ),
30
+ children: (
31
+ <div style={{ padding: '8px 0' }}>
32
+ <ProviderList />
33
+ </div>
34
+ ),
35
+ },
36
+ ];
37
+
38
+ return (
39
+ <div style={{ padding: 24 }}>
40
+ <Tabs items={items} defaultActiveKey="global" />
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,2 @@
1
+ export { default } from './plugin';
2
+ export { PluginDocumentParserClient } from './plugin';
@@ -0,0 +1,12 @@
1
+ import { i18n } from '@nocobase/client';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ export const NAMESPACE = 'plugin-document-parser';
5
+
6
+ export function lang(key: string, options?: any) {
7
+ return i18n.t(key, { ns: NAMESPACE, ...options });
8
+ }
9
+
10
+ export function useDocParserTranslation() {
11
+ return useTranslation(NAMESPACE);
12
+ }