@acms-subir/core 0.2.0 → 0.3.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/CHANGELOG.md +87 -0
- package/cli/common.mjs +35 -0
- package/cli/legacy.mjs +317 -0
- package/cli/main.mjs +66 -0
- package/cli/palette.mjs +4 -0
- package/cli/preview.mjs +50 -0
- package/cli/remote.mjs +55 -0
- package/cli/themes.mjs +127 -0
- package/cli.mjs +2 -369
- package/deploy/Caddyfile +8 -0
- package/deploy/Dockerfile +34 -0
- package/deploy/compose.https.yml +21 -0
- package/deploy/compose.yml +46 -0
- package/docs/live-video.md +91 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +189 -2
- package/ops-check.mjs +27 -0
- package/package.json +27 -5
- package/src/actions/index.ts +578 -49
- package/src/components/AiCleaner.tsx +291 -0
- package/src/components/AiSettings.tsx +575 -0
- package/src/components/CommandPalette.tsx +200 -0
- package/src/components/ContentList.astro +246 -0
- package/src/components/DesignAccess.tsx +182 -0
- package/src/components/DesignManager.tsx +418 -46
- package/src/components/EmailSettings.tsx +521 -0
- package/src/components/EventPage.astro +178 -0
- package/src/components/LegalPage.astro +27 -0
- package/src/components/LinksManager.tsx +119 -58
- package/src/components/LiveCard.astro +174 -0
- package/src/components/LiveManager.tsx +910 -0
- package/src/components/NavManager.tsx +42 -43
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +58 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/WidgetRenderer.astro +3 -0
- package/src/components/booking/BookingFlow.astro +150 -0
- package/src/components/booking/HoursEditor.astro +63 -0
- package/src/components/builder/PageBuilder.tsx +10 -2
- package/src/components/editor/EventPanel.tsx +401 -0
- package/src/components/editor/FormatDock.tsx +81 -0
- package/src/components/editor/LivePane.tsx +122 -0
- package/src/components/editor/MobileToolbar.tsx +93 -0
- package/src/components/editor/PostEditor.tsx +605 -223
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/slash.tsx +7 -1
- package/src/components/site/Pager.astro +24 -0
- package/src/components/site/PostCard.astro +64 -0
- package/src/components/site/TagLabel.astro +11 -0
- package/src/layouts/Admin.astro +289 -49
- package/src/layouts/Site.astro +352 -59
- package/src/lib/agent-api.ts +13 -0
- package/src/lib/agent-discovery.ts +40 -0
- package/src/lib/agent.ts +114 -0
- package/src/lib/ai-config.ts +58 -0
- package/src/lib/ai-schema.ts +22 -0
- package/src/lib/ai.ts +90 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +56 -0
- package/src/lib/auth.ts +25 -5
- package/src/lib/backup/content.ts +36 -0
- package/src/lib/backup/unzip.ts +12 -1
- package/src/lib/backup/zip.ts +1 -1
- package/src/lib/booking/agent.ts +108 -0
- package/src/lib/booking/availability.ts +221 -0
- package/src/lib/booking/emails.ts +115 -0
- package/src/lib/booking/forms.ts +77 -0
- package/src/lib/booking/service.ts +277 -0
- package/src/lib/booking/view.ts +54 -0
- package/src/lib/calendar/agent-schema.ts +48 -0
- package/src/lib/calendar/agent.ts +136 -0
- package/src/lib/calendar/api.ts +81 -0
- package/src/lib/calendar/cards.ts +102 -0
- package/src/lib/calendar/config.ts +52 -0
- package/src/lib/calendar/data.ts +130 -0
- package/src/lib/calendar/events.ts +261 -0
- package/src/lib/calendar/feeds.ts +43 -0
- package/src/lib/calendar/hooks.ts +38 -0
- package/src/lib/calendar/http.ts +30 -0
- package/src/lib/calendar/ics-write.ts +120 -0
- package/src/lib/calendar/ics.ts +15 -5
- package/src/lib/calendar/import.ts +203 -0
- package/src/lib/calendar/render.ts +120 -37
- package/src/lib/calendar/rrule.ts +149 -0
- package/src/lib/calendar/seo.ts +48 -0
- package/src/lib/design-service.ts +58 -0
- package/src/lib/designDiff.ts +145 -0
- package/src/lib/designPreview.ts +110 -12
- package/src/lib/documents/cleanup.ts +31 -0
- package/src/lib/documents/extract.ts +154 -0
- package/src/lib/documents/vendor.d.ts +4 -0
- package/src/lib/drafts.ts +60 -6
- package/src/lib/dynamicBlocks.ts +40 -8
- package/src/lib/email/accounts.ts +55 -0
- package/src/lib/email/config.ts +86 -0
- package/src/lib/email/migration.ts +24 -0
- package/src/lib/email/providers.ts +89 -0
- package/src/lib/email/service.ts +78 -0
- package/src/lib/email/store.ts +167 -0
- package/src/lib/email/templates.ts +18 -0
- package/src/lib/email/types.ts +32 -0
- package/src/lib/email/webhooks.ts +87 -0
- package/src/lib/email.ts +15 -0
- package/src/lib/env.ts +1 -1
- package/src/lib/legal/index.ts +25 -0
- package/src/lib/live/client.ts +88 -0
- package/src/lib/live/crypto.ts +28 -0
- package/src/lib/live/migration.ts +15 -0
- package/src/lib/live/providers.ts +152 -0
- package/src/lib/live/service.ts +170 -0
- package/src/lib/live/signals.ts +89 -0
- package/src/lib/live/store.ts +49 -0
- package/src/lib/live/types.ts +52 -0
- package/src/lib/live/view.ts +78 -0
- package/src/lib/mobile/bookings.ts +73 -0
- package/src/lib/mobile/calendar.ts +85 -0
- package/src/lib/mobile/content.ts +85 -0
- package/src/lib/mobile/http.ts +63 -0
- package/src/lib/mobile/route.ts +20 -0
- package/src/lib/ops.ts +8 -0
- package/src/lib/preview.ts +21 -1
- package/src/lib/repo/migrations.ts +221 -0
- package/src/lib/repo/sql.ts +907 -68
- package/src/lib/repo/types.ts +125 -5
- package/src/lib/scheduler.ts +27 -0
- package/src/lib/schema.ts +316 -1
- package/src/lib/search-i18n.ts +3 -0
- package/src/lib/socialIcons.ts +32 -0
- package/src/lib/tagColor.ts +11 -0
- package/src/lib/theme.ts +59 -40
- package/src/lib/themeTokens.ts +183 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhook-sql.ts +68 -0
- package/src/lib/webhooks.ts +147 -0
- package/src/lib/widgets.ts +1 -0
- package/src/middleware.ts +45 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +14 -5
- package/src/pages/account/[action].astro +53 -0
- package/src/pages/admin/ai.astro +12 -0
- package/src/pages/admin/c/[collection]/[id].astro +4 -1
- package/src/pages/admin/c/[collection]/index.astro +127 -92
- package/src/pages/admin/calendar/bookings.astro +400 -0
- package/src/pages/admin/calendar/calendars.astro +265 -0
- package/src/pages/admin/calendar/events/[id].astro +56 -0
- package/src/pages/admin/calendar/events/index.astro +408 -0
- package/src/pages/admin/calendar/index.ts +4 -0
- package/src/pages/admin/calendar/integrations.astro +4 -0
- package/src/pages/admin/calendar/resources/[id].astro +180 -0
- package/src/pages/admin/calendar/resources/index.astro +106 -0
- package/src/pages/admin/calendar/resources/link.astro +196 -0
- package/src/pages/admin/design.astro +10 -3
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +232 -85
- package/src/pages/admin/integrations.astro +237 -0
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/live.astro +12 -0
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +4 -1
- package/src/pages/admin/pages/index.astro +41 -48
- package/src/pages/admin/settings.astro +418 -367
- package/src/pages/admin/tags.astro +4 -4
- package/src/pages/admin/users.astro +38 -28
- package/src/pages/api/agent/docs.ts +33 -0
- package/src/pages/api/agent/index.ts +21 -0
- package/src/pages/api/ai/clean.ts +13 -0
- package/src/pages/api/ai/draft.ts +14 -0
- package/src/pages/api/ai/settings.ts +53 -0
- package/src/pages/api/ai/test.ts +12 -0
- package/src/pages/api/backup.ts +8 -21
- package/src/pages/api/calendar/availability.ts +35 -0
- package/src/pages/api/calendar/bookings/[id].ts +49 -0
- package/src/pages/api/calendar/bookings/index.ts +73 -0
- package/src/pages/api/calendar/calendars.ts +35 -0
- package/src/pages/api/calendar/docs.ts +66 -0
- package/src/pages/api/calendar/events/[id].ts +63 -0
- package/src/pages/api/calendar/events/index.ts +112 -0
- package/src/pages/api/calendar/resources.ts +34 -0
- package/src/pages/api/design/capabilities.ts +10 -0
- package/src/pages/api/design/index.ts +58 -0
- package/src/pages/api/design/keys.ts +24 -0
- package/src/pages/api/design/media.ts +32 -0
- package/src/pages/api/email/run.ts +11 -0
- package/src/pages/api/email/settings.ts +61 -0
- package/src/pages/api/email/webhooks/[provider].ts +4 -0
- package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
- package/src/pages/api/live/settings.ts +45 -0
- package/src/pages/api/live/status.ts +7 -0
- package/src/pages/api/live/thumb.ts +48 -0
- package/src/pages/api/mcp.ts +59 -0
- package/src/pages/api/media/upload.ts +2 -2
- package/src/pages/api/ops/status.ts +39 -0
- package/src/pages/api/restore.ts +27 -37
- package/src/pages/api/search.ts +18 -4
- package/src/pages/api/v1/availability.ts +7 -0
- package/src/pages/api/v1/bookings.ts +17 -0
- package/src/pages/api/v1/calendars.ts +7 -0
- package/src/pages/api/v1/content/[id].ts +7 -0
- package/src/pages/api/v1/content/index.ts +7 -0
- package/src/pages/api/v1/events.ts +7 -0
- package/src/pages/api/v1/resources.ts +7 -0
- package/src/pages/api/webhooks/docs.ts +61 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +137 -66
- package/src/pages/blog/index.astro +95 -30
- package/src/pages/blog/tag/[slug].astro +45 -40
- package/src/pages/book/[resource]/[link].astro +70 -0
- package/src/pages/book/[resource]/index.astro +63 -0
- package/src/pages/book/[resource].ics.ts +38 -0
- package/src/pages/book/index.astro +47 -0
- package/src/pages/book/manage/[token].astro +108 -0
- package/src/pages/calendar/[slug].ics.ts +27 -0
- package/src/pages/calendar/feed.ics.ts +15 -0
- package/src/pages/calendar/index.astro +191 -0
- package/src/pages/embed/book/[resource]/[link].astro +33 -0
- package/src/pages/embed/calendar/[slug].astro +43 -0
- package/src/pages/events/[slug].astro +74 -0
- package/src/pages/events/[slug].ics.ts +25 -0
- package/src/pages/index.astro +12 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +29 -3
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +27 -6
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +924 -105
- package/src/styles/cards.css +92 -74
- package/src/styles/editor.css +492 -114
- package/src/styles/email-settings.css +117 -0
- package/src/styles/live-admin.css +126 -0
- package/src/styles/live.css +199 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -281
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +81 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +55 -0
- package/src/themes/editorial/templates/home.astro +4 -4
- package/src/themes/editorial/templates/post-minimal.astro +2 -2
- package/src/themes/editorial/theme.css +27 -37
- package/src/themes/editorial/theme.json +25 -3
- package/src/themes/field/theme.json +27 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +28 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { extractDocument } from '../lib/documents/extract';
|
|
3
|
+
|
|
4
|
+
type Kind = 'posts' | 'pages';
|
|
5
|
+
type Props = { initialKind?: Kind; initialCollection?: string; collections: Array<{ key: string; label: string }> };
|
|
6
|
+
type Options = { notes: string; spacing: boolean; grammar: boolean; formatting: boolean; translate: boolean; targetLanguage: string };
|
|
7
|
+
type Doc = { filename: string; source: string; warnings: string[] };
|
|
8
|
+
// `key` fingerprints the source + options the result was produced from; `cleaned` is the untouched model output.
|
|
9
|
+
type Result = { key: string; original: string; cleaned: string; text: string; model: string; warnings: string[]; translatedTo: string; failed: boolean };
|
|
10
|
+
|
|
11
|
+
const MAX_SOURCE = 50000, MAX_NOTES = 2000, MAX_LANG = 100, MAX_TITLE = 300;
|
|
12
|
+
const DEFAULT_OPTIONS: Options = { notes: '', spacing: true, grammar: true, formatting: true, translate: false, targetLanguage: '' };
|
|
13
|
+
|
|
14
|
+
async function request<T>(path: string, body: unknown): Promise<T> {
|
|
15
|
+
const response = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
16
|
+
const data = await response.json().catch(() => null);
|
|
17
|
+
if (!response.ok || !data) throw new Error(data?.error || 'Request failed. Try again.');
|
|
18
|
+
return data as T;
|
|
19
|
+
}
|
|
20
|
+
const message = (cause: unknown, fallback: string) => (cause instanceof Error && cause.message ? cause.message : fallback);
|
|
21
|
+
function payload(source: string, o: Options) {
|
|
22
|
+
return { source, notes: o.notes.trim(), spacing: o.spacing, grammar: o.grammar, formatting: o.formatting, translate: o.translate, targetLanguage: o.translate ? o.targetLanguage.trim() : '' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function AiCleaner({ initialKind = 'posts', initialCollection, collections }: Props) {
|
|
26
|
+
const [kind, setKind] = useState<Kind>(initialKind);
|
|
27
|
+
const [collection, setCollection] = useState(collections.find((c) => c.key === initialCollection)?.key ?? collections[0]?.key ?? '');
|
|
28
|
+
const [doc, setDoc] = useState<Doc | null>(null);
|
|
29
|
+
const [options, setOptions] = useState<Options>(DEFAULT_OPTIONS);
|
|
30
|
+
const [title, setTitle] = useState('');
|
|
31
|
+
const [result, setResult] = useState<Result | null>(null);
|
|
32
|
+
const [reviewed, setReviewed] = useState(false);
|
|
33
|
+
const [editUrl, setEditUrl] = useState('');
|
|
34
|
+
const [busy, setBusy] = useState<'extract' | 'clean' | 'save' | null>(null);
|
|
35
|
+
const [error, setError] = useState('');
|
|
36
|
+
const [notice, setNotice] = useState('');
|
|
37
|
+
const pending = useRef(false);
|
|
38
|
+
const fileInput = useRef<HTMLInputElement>(null);
|
|
39
|
+
|
|
40
|
+
const set = <K extends keyof Options>(key: K, value: Options[K]) => setOptions((o) => ({ ...o, [key]: value }));
|
|
41
|
+
const source = doc?.source ?? '';
|
|
42
|
+
const currentKey = JSON.stringify(payload(source, options));
|
|
43
|
+
const stale = !!result && (result.failed || result.key !== currentKey);
|
|
44
|
+
const locked = !!busy || !!editUrl;
|
|
45
|
+
const needsFlag = !options.translate && !options.spacing && !options.grammar && !options.formatting;
|
|
46
|
+
const cleanProblem = !source.trim() ? 'Upload a document with text to clean.'
|
|
47
|
+
: source.length > MAX_SOURCE ? `The source is over ${MAX_SOURCE.toLocaleString()} characters. Shorten it before cleaning.`
|
|
48
|
+
: needsFlag ? 'Choose at least one cleanup option, or request a translation.'
|
|
49
|
+
: options.translate && !options.targetLanguage.trim() ? 'Enter the language to translate into.'
|
|
50
|
+
: '';
|
|
51
|
+
const saveProblem = !title.trim() ? 'Add a title before creating the draft.'
|
|
52
|
+
: !result?.text.trim() ? 'The cleaned content is empty.'
|
|
53
|
+
: (result?.text.length ?? 0) > 100000 ? 'The cleaned content exceeds 100,000 characters.'
|
|
54
|
+
: !reviewed ? 'Confirm you have reviewed the content.'
|
|
55
|
+
: '';
|
|
56
|
+
const hasWork = !!doc || !!result;
|
|
57
|
+
|
|
58
|
+
async function pickFile(file: File | undefined) {
|
|
59
|
+
if (!file || pending.current) return;
|
|
60
|
+
if (hasWork && !editUrl && !confirm('Replace the current document? Its source text and any cleaned result will be discarded.')) {
|
|
61
|
+
if (fileInput.current) fileInput.current.value = '';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Drop everything tied to the previous file before reading the new one, so a failure can't leave it behind.
|
|
65
|
+
pending.current = true; setBusy('extract'); setError(''); setNotice(`Reading ${file.name}…`);
|
|
66
|
+
setDoc(null); setResult(null); setTitle(''); setReviewed(false); setEditUrl('');
|
|
67
|
+
setOptions((previous) => ({ ...previous, translate: false, targetLanguage: '' }));
|
|
68
|
+
try {
|
|
69
|
+
const extracted = await extractDocument(file);
|
|
70
|
+
if (!extracted.source.trim()) throw new Error('No text was found in this document.');
|
|
71
|
+
setDoc({ filename: extracted.filename, source: extracted.source, warnings: extracted.warnings });
|
|
72
|
+
setTitle(extracted.title.slice(0, MAX_TITLE));
|
|
73
|
+
setNotice(`Loaded ${extracted.filename}. Check the extracted text, then clean it up.`);
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
if (fileInput.current) fileInput.current.value = '';
|
|
76
|
+
setNotice(''); setError(message(cause, 'Could not read this document. Try a .docx, text-based .pdf, .txt or .md file.'));
|
|
77
|
+
} finally { pending.current = false; setBusy(null); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function clean() {
|
|
81
|
+
if (pending.current || cleanProblem || editUrl) return;
|
|
82
|
+
if (result && !result.failed && result.text !== result.cleaned && !confirm('Clean again? Your edits to the cleaned result will be replaced.')) return;
|
|
83
|
+
const body = payload(source, options), key = currentKey;
|
|
84
|
+
pending.current = true; setBusy('clean'); setError(''); setNotice('Cleaning up the document…');
|
|
85
|
+
try {
|
|
86
|
+
const res = await request<{ text: string; model: string; warnings?: string[] }>('/api/ai/clean', body);
|
|
87
|
+
setResult({ key, original: body.source, cleaned: res.text, text: res.text, model: res.model, warnings: res.warnings ?? [], translatedTo: body.targetLanguage, failed: false });
|
|
88
|
+
setReviewed(false);
|
|
89
|
+
setNotice('Cleanup finished. Compare it with the original and edit before creating a draft.');
|
|
90
|
+
} catch (cause) {
|
|
91
|
+
// Keep the previous text for reference, but it no longer matches what was requested.
|
|
92
|
+
setResult((r) => (r ? { ...r, failed: true } : r));
|
|
93
|
+
setNotice(''); setError(message(cause, 'Cleanup failed. Try again.'));
|
|
94
|
+
} finally { pending.current = false; setBusy(null); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function save() {
|
|
98
|
+
if (pending.current || editUrl || stale || saveProblem || !result) return;
|
|
99
|
+
pending.current = true; setBusy('save'); setError(''); setNotice('Creating draft…');
|
|
100
|
+
try {
|
|
101
|
+
const body = kind === 'posts' && collection
|
|
102
|
+
? { kind, collection, title: title.trim(), content: result.text }
|
|
103
|
+
: { kind, title: title.trim(), content: result.text };
|
|
104
|
+
const res = await request<{ id: string; status: 'draft'; editUrl: string }>('/api/ai/draft', body);
|
|
105
|
+
setEditUrl(res.editUrl);
|
|
106
|
+
setNotice('Draft created. Opening the editor…');
|
|
107
|
+
window.location.assign(res.editUrl);
|
|
108
|
+
} catch (cause) {
|
|
109
|
+
setNotice(''); setError(message(cause, 'Could not create the draft.'));
|
|
110
|
+
} finally { pending.current = false; setBusy(null); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function startOver() {
|
|
114
|
+
if (pending.current) return;
|
|
115
|
+
if (!editUrl && hasWork && !confirm('Discard this document and its cleaned result?')) return;
|
|
116
|
+
setDoc(null); setResult(null); setTitle(''); setReviewed(false); setEditUrl(''); setOptions(DEFAULT_OPTIONS);
|
|
117
|
+
setError(''); setNotice('Cleared. Upload another document to begin.');
|
|
118
|
+
if (fileInput.current) fileInput.current.value = '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const kindLabel = kind === 'posts' ? 'post' : 'page';
|
|
122
|
+
return <div className="aic">
|
|
123
|
+
<style>{CSS}</style>
|
|
124
|
+
<div className="page-head">
|
|
125
|
+
<div><h1>Create from document</h1><p className="sub">Upload a document, clean up its spacing, spelling and formatting, then turn it into a draft post or page. Its meaning and language stay as written.</p></div>
|
|
126
|
+
<div className="aic-head-actions">
|
|
127
|
+
{hasWork && <button type="button" className="btn" onClick={startOver} disabled={!!busy}>Start over</button>}
|
|
128
|
+
<a className="btn" href="/admin/settings#ai-agents">AI settings</a>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="aic-grid">
|
|
133
|
+
<form className="aic-card" aria-labelledby="aic-source-heading" aria-busy={busy === 'extract' || busy === 'clean'} onSubmit={(e) => { e.preventDefault(); void clean(); }}>
|
|
134
|
+
<div className="aic-kicker">01 · Upload & clean</div>
|
|
135
|
+
<h2 id="aic-source-heading">Your document</h2>
|
|
136
|
+
<fieldset disabled={locked} className="aic-fs">
|
|
137
|
+
<div className="field-row">
|
|
138
|
+
<label className="field">
|
|
139
|
+
<span>Create a</span>
|
|
140
|
+
<select value={kind} onChange={(e) => setKind(e.target.value as Kind)}>
|
|
141
|
+
<option value="posts">Post</option>
|
|
142
|
+
<option value="pages">Page</option>
|
|
143
|
+
</select>
|
|
144
|
+
</label>
|
|
145
|
+
{kind === 'posts' && collections.length > 1 && <label className="field">
|
|
146
|
+
<span>Collection</span>
|
|
147
|
+
<select value={collection} onChange={(e) => setCollection(e.target.value)}>
|
|
148
|
+
{collections.map((c) => <option key={c.key} value={c.key}>{c.label}</option>)}
|
|
149
|
+
</select>
|
|
150
|
+
</label>}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<label className="field">
|
|
154
|
+
<span>Document</span>
|
|
155
|
+
<input ref={fileInput} type="file" className="aic-file" accept=".docx,.pdf,.txt,.md,text/plain,text/markdown,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-describedby="aic-file-hint" onChange={(e) => void pickFile(e.target.files?.[0])} />
|
|
156
|
+
<small className="hint" id="aic-file-hint">DOCX, text-based PDF, TXT or Markdown, up to 10 MB. The file is read in your browser; scanned or password-protected PDFs aren't supported.</small>
|
|
157
|
+
</label>
|
|
158
|
+
{busy === 'extract' && <p className="aic-inline">Reading document…</p>}
|
|
159
|
+
|
|
160
|
+
{doc && <>
|
|
161
|
+
{doc.warnings.length > 0 && <Warnings title={`Notes on ${doc.filename}`} items={doc.warnings} />}
|
|
162
|
+
<label className="field">
|
|
163
|
+
<span>Extracted text <span className="hint">· {doc.filename} · {source.length.toLocaleString()} / {MAX_SOURCE.toLocaleString()}</span></span>
|
|
164
|
+
<textarea dir="auto" rows={10} maxLength={MAX_SOURCE} value={source} onChange={(e) => setDoc({ ...doc, source: e.target.value })} aria-describedby="aic-source-hint" />
|
|
165
|
+
<small className="hint" id="aic-source-hint">Fix anything the import got wrong. This is the text that will be cleaned.</small>
|
|
166
|
+
</label>
|
|
167
|
+
|
|
168
|
+
<fieldset className="aic-group">
|
|
169
|
+
<legend>What to clean</legend>
|
|
170
|
+
<label className="check-row"><input type="checkbox" checked={options.spacing} onChange={(e) => set('spacing', e.target.checked)} /> Spacing, blank lines and paragraph breaks</label>
|
|
171
|
+
<label className="check-row"><input type="checkbox" checked={options.grammar} onChange={(e) => set('grammar', e.target.checked)} /> Spelling, grammar and punctuation</label>
|
|
172
|
+
<label className="check-row"><input type="checkbox" checked={options.formatting} onChange={(e) => set('formatting', e.target.checked)} /> Consistent headings, lists and text formatting</label>
|
|
173
|
+
</fieldset>
|
|
174
|
+
|
|
175
|
+
<div className="aic-note">Keeps the author's meaning, voice, names, facts, dates, numbers and section order. Cleanup is limited to corrections; unclear passages should be flagged for your review. The document keeps its original language, including mixed-language passages. Its text is treated as content to clean, never as instructions.</div>
|
|
176
|
+
|
|
177
|
+
<label className="field">
|
|
178
|
+
<span>Cleanup notes <span className="hint">optional</span></span>
|
|
179
|
+
<textarea rows={2} maxLength={MAX_NOTES} value={options.notes} onChange={(e) => set('notes', e.target.value)} placeholder="For example: keep the informal tone" />
|
|
180
|
+
</label>
|
|
181
|
+
|
|
182
|
+
<label className="check-row"><input type="checkbox" checked={options.translate} onChange={(e) => set('translate', e.target.checked)} aria-describedby="aic-translate-hint" /> I want this translated</label>
|
|
183
|
+
<small className="hint aic-under-check" id="aic-translate-hint">Off unless you tick it. Requests written inside the document don't count.</small>
|
|
184
|
+
{options.translate && <div className="subpanel">
|
|
185
|
+
<label className="field">
|
|
186
|
+
<span>Translate into</span>
|
|
187
|
+
<input type="text" required maxLength={MAX_LANG} value={options.targetLanguage} onChange={(e) => set('targetLanguage', e.target.value)} placeholder="Language name, e.g. Spanish" />
|
|
188
|
+
</label>
|
|
189
|
+
</div>}
|
|
190
|
+
</>}
|
|
191
|
+
</fieldset>
|
|
192
|
+
|
|
193
|
+
{doc && <div className="aic-actions">
|
|
194
|
+
<button className="btn primary" type="submit" disabled={locked || !!cleanProblem}>{busy === 'clean' ? 'Cleaning…' : result ? 'Clean again' : 'Clean up document'}</button>
|
|
195
|
+
{cleanProblem && !busy && <span className="hint">{cleanProblem}</span>}
|
|
196
|
+
</div>}
|
|
197
|
+
<p className="hint aic-privacy">Only the extracted text, your notes and options are sent to the AI provider set in Settings, and only when you click. Nothing is stored in your browser.</p>
|
|
198
|
+
</form>
|
|
199
|
+
|
|
200
|
+
<section className="aic-card" aria-labelledby="aic-review-heading" aria-busy={busy === 'save'}>
|
|
201
|
+
<div className="aic-kicker">02 · Review & create draft</div>
|
|
202
|
+
<h2 id="aic-review-heading">Compare and edit</h2>
|
|
203
|
+
{!result ? <p className="aic-empty">{doc ? 'Clean up the document to compare the result with the original here.' : 'Upload a document to begin.'}</p> : <>
|
|
204
|
+
<div className="aic-meta">
|
|
205
|
+
<span className="aic-badge">{result.translatedTo ? `Cleaned and translated into ${result.translatedTo}` : 'Cleaned · no translation requested'}</span>
|
|
206
|
+
{result.model && <span className="hint">Model: {result.model}</span>}
|
|
207
|
+
</div>
|
|
208
|
+
{stale && <div className="aic-stale" role="note">
|
|
209
|
+
<b>{result.failed ? 'The last cleanup failed.' : 'The source or options changed.'}</b> This result doesn't reflect the current document and options. Clean again before creating a draft.
|
|
210
|
+
</div>}
|
|
211
|
+
{result.warnings.length > 0 && <Warnings title="Flagged for review" items={result.warnings} />}
|
|
212
|
+
<div className="aic-compare">
|
|
213
|
+
<label className="field">
|
|
214
|
+
<span>Original <span className="hint">as sent for cleanup · read-only</span></span>
|
|
215
|
+
<textarea dir="auto" readOnly rows={18} value={result.original} className="aic-original" />
|
|
216
|
+
</label>
|
|
217
|
+
<label className="field">
|
|
218
|
+
<span>Cleaned <span className="hint">Markdown · editable</span></span>
|
|
219
|
+
<textarea dir="auto" rows={18} maxLength={100000} value={result.text} onChange={(e) => { setResult({ ...result, text: e.target.value }); }} disabled={locked} />
|
|
220
|
+
</label>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{!stale && <fieldset className="aic-fs aic-save" disabled={locked}>
|
|
224
|
+
<legend className="sr-only">Draft details</legend>
|
|
225
|
+
<label className="field">
|
|
226
|
+
<span>Title</span>
|
|
227
|
+
<input type="text" dir="auto" required maxLength={MAX_TITLE} value={title} onChange={(e) => setTitle(e.target.value)} aria-describedby="aic-title-hint" />
|
|
228
|
+
<small className="hint" id="aic-title-hint">Taken from the document's heading or file name. It isn't cleaned or translated, so edit it here.</small>
|
|
229
|
+
</label>
|
|
230
|
+
<label className="check-row"><input type="checkbox" checked={reviewed} onChange={(e) => setReviewed(e.target.checked)} /> I've compared the cleaned content with the original</label>
|
|
231
|
+
</fieldset>}
|
|
232
|
+
|
|
233
|
+
{!stale && <div className="aic-actions">
|
|
234
|
+
{editUrl
|
|
235
|
+
? <a className="btn primary" href={editUrl}>Open draft in editor →</a>
|
|
236
|
+
: <button type="button" className="btn primary" onClick={() => void save()} disabled={locked || !!saveProblem}>{busy === 'save' ? 'Creating…' : 'Create draft & open editor'}</button>}
|
|
237
|
+
{!editUrl && saveProblem && !busy && <span className="hint">{saveProblem}</span>}
|
|
238
|
+
</div>}
|
|
239
|
+
<p className="hint aic-privacy">Creates a new unpublished {kindLabel}. Nothing goes live until you publish it from the editor.</p>
|
|
240
|
+
</>}
|
|
241
|
+
</section>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{error && <div className="flash error" role="alert">{error}</div>}
|
|
245
|
+
<p className="aic-status" role="status" aria-live="polite">{notice}</p>
|
|
246
|
+
</div>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function Warnings({ title, items }: { title: string; items: string[] }) {
|
|
250
|
+
return <div className="aic-warn">
|
|
251
|
+
<b>{title}</b>
|
|
252
|
+
<ul>{items.map((w, i) => <li key={i}>{w}</li>)}</ul>
|
|
253
|
+
</div>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const CSS = `
|
|
257
|
+
.aic{max-width:1280px;margin:0 auto}
|
|
258
|
+
.aic-head-actions{display:flex;gap:8px;flex-wrap:wrap}
|
|
259
|
+
.aic-grid{display:grid;grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:20px;align-items:start}
|
|
260
|
+
.aic-card{min-width:0;padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}
|
|
261
|
+
.aic-kicker{font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:600;color:var(--accent-deep);margin-bottom:8px}
|
|
262
|
+
.aic h2{margin:0 0 18px;font-size:19px;letter-spacing:-.02em}
|
|
263
|
+
.aic-fs{border:0;margin:0;padding:0;min-width:0}
|
|
264
|
+
.aic-group{border:0;margin:0 0 6px;padding:0}
|
|
265
|
+
.aic-group legend{font-size:12px;color:var(--muted);font-weight:500;margin-bottom:8px;padding:0}
|
|
266
|
+
.aic textarea{width:100%}
|
|
267
|
+
.aic-file{display:block;width:100%;font:inherit;color:var(--text-2);padding:10px;border:1.5px dashed var(--line-strong);border-radius:var(--r-md);background:var(--panel-2);cursor:pointer}
|
|
268
|
+
.aic-file:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
|
|
269
|
+
.aic-file::file-selector-button{font:inherit;font-weight:500;margin-right:10px;padding:5px 12px;border:1px solid var(--line-strong);border-radius:var(--r-sm);background:var(--panel);color:var(--text);cursor:pointer}
|
|
270
|
+
.aic-fs:disabled .aic-file{cursor:not-allowed;opacity:.7}
|
|
271
|
+
.aic-note{background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r-md);padding:10px 13px;font-size:12.5px;color:var(--text-2);margin:6px 0 14px}
|
|
272
|
+
.aic-under-check{display:block;margin:-6px 0 10px 23px}
|
|
273
|
+
.aic .subpanel{margin:0 0 12px 23px}.aic .subpanel .field{margin-bottom:0}
|
|
274
|
+
.aic-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px}
|
|
275
|
+
.aic-privacy{margin:12px 0 0}
|
|
276
|
+
.aic-inline{color:var(--muted);margin:0 0 12px}
|
|
277
|
+
.aic-empty{color:var(--muted);margin:0;padding:40px 16px;text-align:center;background:var(--panel-2);border-radius:var(--r-md)}
|
|
278
|
+
.aic-meta{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
|
|
279
|
+
.aic-badge{background:var(--accent-soft);color:var(--accent-deep);border-radius:var(--r-full);padding:3px 10px;font-size:12px;font-weight:600}
|
|
280
|
+
.aic-stale{background:var(--amber-soft);color:var(--amber);border-radius:var(--r-md);padding:10px 13px;margin-bottom:12px;font-size:13px}
|
|
281
|
+
.aic-warn{background:var(--amber-soft);color:var(--amber);border-radius:var(--r-md);padding:10px 13px;margin-bottom:12px;font-size:12.5px}
|
|
282
|
+
.aic-warn ul{margin:4px 0 0;padding-left:18px}
|
|
283
|
+
.aic-compare{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
284
|
+
.aic-compare textarea{min-height:340px}
|
|
285
|
+
.aic-original{background:var(--panel-2);color:var(--text-2)}
|
|
286
|
+
.aic-save{margin-top:4px;padding-top:14px;border-top:1px solid var(--line)}
|
|
287
|
+
.aic-status{color:var(--green);min-height:22px;margin-top:14px}
|
|
288
|
+
.aic .flash{margin-top:14px}
|
|
289
|
+
@media(max-width:1100px){.aic-grid{grid-template-columns:1fr}}
|
|
290
|
+
@media(max-width:720px){.aic-card{padding:18px}.aic-compare,.aic .field-row{grid-template-columns:1fr}.aic-compare textarea{min-height:220px}}
|
|
291
|
+
`;
|