@growth-labs/cms 0.5.5 → 0.5.7
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.md +46 -0
- package/dist/engine/ai-dek-notifications.d.ts +16 -0
- package/dist/engine/ai-dek-notifications.d.ts.map +1 -0
- package/dist/engine/ai-dek-notifications.js +34 -0
- package/dist/engine/ai-dek-notifications.js.map +1 -0
- package/dist/engine/ai-dek.d.ts +35 -0
- package/dist/engine/ai-dek.d.ts.map +1 -0
- package/dist/engine/ai-dek.js +99 -0
- package/dist/engine/ai-dek.js.map +1 -0
- package/dist/engine/ai-prompts.d.ts +12 -0
- package/dist/engine/ai-prompts.d.ts.map +1 -1
- package/dist/engine/ai-prompts.js +17 -0
- package/dist/engine/ai-prompts.js.map +1 -1
- package/dist/engine/index.d.ts +3 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +4 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/index.d.ts.map +1 -1
- package/dist/integration/index.js +13 -0
- package/dist/integration/index.js.map +1 -1
- package/dist/integration/options.d.ts +19 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +7 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/providers/null.d.ts +2 -1
- package/dist/providers/null.d.ts.map +1 -1
- package/dist/providers/null.js +9 -0
- package/dist/providers/null.js.map +1 -1
- package/dist/providers/types.d.ts +10 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/ai.d.ts +2 -0
- package/dist/routes/ai.d.ts.map +1 -1
- package/dist/routes/ai.js +42 -0
- package/dist/routes/ai.js.map +1 -1
- package/dist/routes/authz-matrix.d.ts +2 -1
- package/dist/routes/authz-matrix.d.ts.map +1 -1
- package/dist/routes/authz-matrix.js +5 -0
- package/dist/routes/authz-matrix.js.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +43 -1
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/context.d.ts +1 -1
- package/dist/routes/context.d.ts.map +1 -1
- package/dist/routes/context.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/surveys.d.ts +12 -0
- package/dist/routes/surveys.d.ts.map +1 -0
- package/dist/routes/surveys.js +109 -0
- package/dist/routes/surveys.js.map +1 -0
- package/dist/schema/insights-ingest.d.ts +46 -46
- package/dist/surveys/access.d.ts +4 -0
- package/dist/surveys/access.d.ts.map +1 -0
- package/dist/surveys/access.js +9 -0
- package/dist/surveys/access.js.map +1 -0
- package/dist/surveys/errors.d.ts +43 -0
- package/dist/surveys/errors.d.ts.map +1 -0
- package/dist/surveys/errors.js +42 -0
- package/dist/surveys/errors.js.map +1 -0
- package/dist/surveys/schema.d.ts +939 -0
- package/dist/surveys/schema.d.ts.map +1 -0
- package/dist/surveys/schema.js +385 -0
- package/dist/surveys/schema.js.map +1 -0
- package/dist/ui/api/_content-config.d.ts.map +1 -1
- package/dist/ui/api/_content-config.js +1 -0
- package/dist/ui/api/_content-config.js.map +1 -1
- package/dist/ui/api/content/[id]/ai/dek.d.ts +3 -0
- package/dist/ui/api/content/[id]/ai/dek.d.ts.map +1 -0
- package/dist/ui/api/content/[id]/ai/dek.js +5 -0
- package/dist/ui/api/content/[id]/ai/dek.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.js +17 -0
- package/dist/ui/api/surveys/[surveyId]/responses.csv.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId].d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId].d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId].js +17 -0
- package/dist/ui/api/surveys/[surveyId].js.map +1 -0
- package/dist/ui/client/mount.d.ts.map +1 -1
- package/dist/ui/client/mount.js +1 -0
- package/dist/ui/client/mount.js.map +1 -1
- package/dist/ui/commands.d.ts +5 -1
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +7 -0
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/components/CmsApp.d.ts +2 -0
- package/dist/ui/components/CmsApp.d.ts.map +1 -1
- package/dist/ui/components/CmsApp.js +14 -3
- package/dist/ui/components/CmsApp.js.map +1 -1
- package/dist/ui/components/CommandPalette.d.ts +2 -0
- package/dist/ui/components/CommandPalette.d.ts.map +1 -1
- package/dist/ui/components/CommandPalette.js +1 -1
- package/dist/ui/components/CommandPalette.js.map +1 -1
- package/dist/ui/components/Sidebar.d.ts +1 -0
- package/dist/ui/components/Sidebar.d.ts.map +1 -1
- package/dist/ui/components/Sidebar.js +1 -1
- package/dist/ui/components/Sidebar.js.map +1 -1
- package/dist/ui/editor/AiAssistPanel.d.ts +3 -1
- package/dist/ui/editor/AiAssistPanel.d.ts.map +1 -1
- package/dist/ui/editor/AiAssistPanel.js +28 -4
- package/dist/ui/editor/AiAssistPanel.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +1 -1
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/ai-assist.d.ts +2 -2
- package/dist/ui/editor/ai-assist.d.ts.map +1 -1
- package/dist/ui/hash-router.d.ts +1 -1
- package/dist/ui/hash-router.d.ts.map +1 -1
- package/dist/ui/hash-router.js +1 -0
- package/dist/ui/hash-router.js.map +1 -1
- package/dist/ui/nav.d.ts +5 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +10 -2
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/screens/SurveyResponsesScreen.d.ts +28 -0
- package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -0
- package/dist/ui/screens/SurveyResponsesScreen.js +110 -0
- package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -0
- package/dist/ui/screens/registry.d.ts +2 -1
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +5 -1
- package/dist/ui/screens/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/ai-dek-notifications.ts +50 -0
- package/src/engine/ai-dek.ts +138 -0
- package/src/engine/ai-prompts.ts +25 -0
- package/src/engine/index.ts +14 -0
- package/src/index.ts +2 -2
- package/src/integration/index.ts +13 -0
- package/src/integration/options.ts +8 -0
- package/src/providers/null.ts +19 -0
- package/src/providers/types.ts +7 -0
- package/src/routes/ai.ts +44 -0
- package/src/routes/authz-matrix.ts +14 -1
- package/src/routes/content.ts +45 -1
- package/src/routes/context.ts +3 -0
- package/src/routes/index.ts +6 -0
- package/src/routes/surveys.ts +130 -0
- package/src/surveys/access.ts +11 -0
- package/src/surveys/errors.ts +51 -0
- package/src/surveys/schema.ts +435 -0
- package/src/ui/api/_content-config.ts +2 -0
- package/src/ui/api/content/[id]/ai/dek.ts +8 -0
- package/src/ui/api/surveys/[surveyId]/responses.csv.ts +23 -0
- package/src/ui/api/surveys/[surveyId].ts +23 -0
- package/src/ui/client/mount.tsx +1 -0
- package/src/ui/commands.ts +14 -1
- package/src/ui/components/CmsApp.tsx +25 -3
- package/src/ui/components/CommandPalette.tsx +2 -1
- package/src/ui/components/Sidebar.tsx +10 -2
- package/src/ui/editor/AiAssistPanel.tsx +55 -3
- package/src/ui/editor/ContentForm.tsx +3 -1
- package/src/ui/editor/ai-assist.ts +2 -2
- package/src/ui/hash-router.ts +1 -0
- package/src/ui/nav.ts +15 -2
- package/src/ui/screens/SurveyResponsesScreen.tsx +271 -0
- package/src/ui/screens/registry.tsx +6 -0
package/dist/ui/nav.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/ui/nav.ts — the Masthead sidebar nav model. Two groups, 11 items
|
|
2
2
|
// (spec §6: Social Share added between Subscriptions and Settings).
|
|
3
|
+
import { canViewSurveyResponses } from '../surveys/access.js';
|
|
3
4
|
export const NAV_MODEL = [
|
|
4
5
|
{
|
|
5
6
|
group: 'Workspace',
|
|
@@ -18,16 +19,23 @@ export const NAV_MODEL = [
|
|
|
18
19
|
{ id: 'topics', label: 'Topics & tags', icon: 'tag' },
|
|
19
20
|
{ id: 'content-insights', label: 'Content Insights', icon: 'sparkle' },
|
|
20
21
|
{ id: 'subscriptions', label: 'Subscriptions', icon: 'inbox' },
|
|
22
|
+
{ id: 'survey-responses', label: 'Survey responses', icon: 'chart' },
|
|
21
23
|
{ id: 'social-share', label: 'Social Share', icon: 'share' },
|
|
22
24
|
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
|
23
25
|
],
|
|
24
26
|
},
|
|
25
27
|
];
|
|
26
28
|
/** Build the nav with live counts merged onto matching item ids. */
|
|
27
|
-
export function navItems(counts = {}
|
|
29
|
+
export function navItems(counts = {}, options = {
|
|
30
|
+
surveyResultsEnabled: true,
|
|
31
|
+
role: 'owner',
|
|
32
|
+
}) {
|
|
33
|
+
const canViewSurvey = options.surveyResultsEnabled !== false && canViewSurveyResponses(options.role);
|
|
28
34
|
return NAV_MODEL.map((g) => ({
|
|
29
35
|
group: g.group,
|
|
30
|
-
items: g.items
|
|
36
|
+
items: g.items
|
|
37
|
+
.filter((item) => item.id !== 'survey-responses' || canViewSurvey)
|
|
38
|
+
.map((i) => (counts[i.id] != null ? { ...i, count: counts[i.id] } : { ...i })),
|
|
31
39
|
}));
|
|
32
40
|
}
|
|
33
41
|
//# sourceMappingURL=nav.js.map
|
package/dist/ui/nav.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav.js","sourceRoot":"","sources":["../../src/ui/nav.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,oEAAoE;
|
|
1
|
+
{"version":3,"file":"nav.js","sourceRoot":"","sources":["../../src/ui/nav.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,oEAAoE;AAGpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAe7D,MAAM,CAAC,MAAM,SAAS,GAAe;IACpC;QACC,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE;YAC1D,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;YACpD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;YACvD,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9C,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE;SACtD;KACD;IACD;QACC,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE;YACrD,EAAE,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,EAAE;YACtE,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9D,EAAE,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,OAAO,EAAE;YACpE,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;SACvD;KACD;CACD,CAAA;AAED,oEAAoE;AACpE,MAAM,UAAU,QAAQ,CACvB,SAAyC,EAAE,EAC3C,UAAqE;IACpE,oBAAoB,EAAE,IAAI;IAC1B,IAAI,EAAE,OAAO;CACb;IAED,MAAM,aAAa,GAClB,OAAO,CAAC,oBAAoB,KAAK,KAAK,IAAI,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK;aACZ,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,kBAAkB,IAAI,aAAa,CAAC;aACjE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;KAC/E,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type SurveyResults } from '../../surveys/schema.js';
|
|
2
|
+
export type SurveyResponsesState = {
|
|
3
|
+
kind: 'loading';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'ready';
|
|
6
|
+
data: SurveyResults;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'forbidden';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'unavailable';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'malformed';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'not_found';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'retryable';
|
|
17
|
+
};
|
|
18
|
+
export declare function parseSurveyResultsPayload(value: unknown): SurveyResults | null;
|
|
19
|
+
export declare function classifySurveyResultsResponse(response: Response): Promise<SurveyResponsesState>;
|
|
20
|
+
export declare function SurveyResponsesScreen({ surveyId }: {
|
|
21
|
+
surveyId: string;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
export declare function SurveyResponsesBody({ surveyId, state, onRetry, }: {
|
|
24
|
+
surveyId: string;
|
|
25
|
+
state: SurveyResponsesState;
|
|
26
|
+
onRetry?: () => void;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
//# sourceMappingURL=SurveyResponsesScreen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SurveyResponsesScreen.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/SurveyResponsesScreen.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAA;AAExB,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAG9E;AAED,wBAAsB,6BAA6B,CAClD,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAkB/B;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAiBvE;AAED,wBAAgB,mBAAmB,CAAC,EACnC,QAAQ,EACR,KAAK,EACL,OAAO,GACP,EAAE;IACF,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,oBAAoB,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,+BA8HA"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { isSurveyErrorCode, SURVEY_ERROR_CODE_HEADER, SURVEY_ERROR_DEFINITIONS, } from '../../surveys/errors.js';
|
|
4
|
+
import { SurveyResultsSchema } from '../../surveys/schema.js';
|
|
5
|
+
export function parseSurveyResultsPayload(value) {
|
|
6
|
+
const parsed = SurveyResultsSchema.safeParse(value);
|
|
7
|
+
return parsed.success ? parsed.data : null;
|
|
8
|
+
}
|
|
9
|
+
export async function classifySurveyResultsResponse(response) {
|
|
10
|
+
if (response.ok) {
|
|
11
|
+
const data = parseSurveyResultsPayload(await response.json().catch(() => null));
|
|
12
|
+
return data ? { kind: 'ready', data } : { kind: 'malformed' };
|
|
13
|
+
}
|
|
14
|
+
const code = await readSurveyErrorCode(response);
|
|
15
|
+
if ((response.status === 401 || response.status === 403) && code === 'survey_forbidden') {
|
|
16
|
+
return { kind: 'forbidden' };
|
|
17
|
+
}
|
|
18
|
+
if (response.status === 404 && code === 'survey_not_found')
|
|
19
|
+
return { kind: 'not_found' };
|
|
20
|
+
if (response.status === 502 && code === 'survey_provider_malformed') {
|
|
21
|
+
return { kind: 'malformed' };
|
|
22
|
+
}
|
|
23
|
+
if (response.status === 503 && code === 'survey_provider_unavailable') {
|
|
24
|
+
return { kind: 'unavailable' };
|
|
25
|
+
}
|
|
26
|
+
return { kind: 'retryable' };
|
|
27
|
+
}
|
|
28
|
+
export function SurveyResponsesScreen({ surveyId }) {
|
|
29
|
+
const [state, setState] = useState({ kind: 'loading' });
|
|
30
|
+
const load = useCallback(async () => {
|
|
31
|
+
setState({ kind: 'loading' });
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`/admin/api/surveys/${encodeURIComponent(surveyId)}`);
|
|
34
|
+
setState(await classifySurveyResultsResponse(response));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
setState({ kind: 'retryable' });
|
|
38
|
+
}
|
|
39
|
+
}, [surveyId]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
void load();
|
|
42
|
+
}, [load]);
|
|
43
|
+
return _jsx(SurveyResponsesBody, { surveyId: surveyId, state: state, onRetry: load });
|
|
44
|
+
}
|
|
45
|
+
export function SurveyResponsesBody({ surveyId, state, onRetry, }) {
|
|
46
|
+
if (state.kind === 'loading')
|
|
47
|
+
return _jsx(StatusPage, { title: "Loading survey responses\u2026" });
|
|
48
|
+
if (state.kind === 'forbidden')
|
|
49
|
+
return _jsx(StatusPage, { title: "You do not have permission to view survey responses." });
|
|
50
|
+
if (state.kind === 'unavailable')
|
|
51
|
+
return _jsx(StatusPage, { title: "Survey reporting is not configured for this site." });
|
|
52
|
+
if (state.kind === 'malformed')
|
|
53
|
+
return _jsx(StatusPage, { title: "Masthead received invalid survey data.", retry: onRetry });
|
|
54
|
+
if (state.kind === 'not_found')
|
|
55
|
+
return _jsx(StatusPage, { title: "The configured survey could not be found." });
|
|
56
|
+
if (state.kind === 'retryable')
|
|
57
|
+
return _jsx(StatusPage, { title: "Survey responses could not be loaded.", retry: onRetry });
|
|
58
|
+
const { data } = state;
|
|
59
|
+
const freeTextByQuestion = new Map();
|
|
60
|
+
for (const entry of data.freeText) {
|
|
61
|
+
const entries = freeTextByQuestion.get(entry.questionId) ?? [];
|
|
62
|
+
entries.push(entry);
|
|
63
|
+
freeTextByQuestion.set(entry.questionId, entries);
|
|
64
|
+
}
|
|
65
|
+
return (_jsxs("div", { className: "page page-wide fade-in", style: { maxWidth: 1340 }, children: [_jsxs("div", { className: "page-head", children: [_jsxs("div", { children: [_jsx("h1", { className: "page-title", children: data.survey.title }), _jsxs("div", { className: "page-sub", children: [data.survey.responseCount, " ", data.survey.responseCount === 1 ? 'response' : 'responses', ' ', "\u00B7 ", data.survey.status] })] }), _jsx("a", { className: "btn", href: `/admin/api/surveys/${encodeURIComponent(surveyId)}/responses.csv`, children: "Export CSV" })] }), data.survey.responseCount === 0 ? (_jsxs("div", { className: "card", role: "status", children: [_jsx("h2", { className: "card-title", children: "No responses yet" }), _jsx("p", { className: "page-sub", children: "Aggregates and free-text answers will appear after submission." })] })) : (_jsxs("section", { className: "card", "aria-labelledby": "survey-response-list", children: [_jsx("h2", { className: "card-title", id: "survey-response-list", children: "Recent responses" }), _jsx("div", { className: "masthead-rule" }), _jsxs("table", { className: "tbl", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Submitted" }), _jsx("th", { children: "Channel" }), _jsx("th", { children: "Audience" }), _jsx("th", { children: "Attribution" })] }) }), _jsx("tbody", { children: data.responses.map((response) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx("time", { dateTime: response.submittedAt, children: formatTime(response.submittedAt) }) }), _jsx("td", { children: response.channel }), _jsx("td", { children: segmentLabel(response.respondentSegment) }), _jsx("td", { children: response.anonymous ? 'Anonymous' : 'Known respondent' })] }, response.id))) })] })] })), _jsxs("section", { "aria-labelledby": "survey-aggregates", style: { marginTop: 24 }, children: [_jsx("h2", { className: "page-title", id: "survey-aggregates", style: { fontSize: 22 }, children: "Question aggregates" }), _jsx("div", { className: "grid-split-wide", style: { alignItems: 'start', marginTop: 12 }, children: data.questions.map((question) => (_jsxs("article", { className: "card", children: [_jsxs("div", { className: "kicker", children: ["Q", question.position + 1, " \u00B7 ", questionAudienceLabel(question.audience)] }), _jsx("h3", { className: "card-title", children: question.prompt }), question.counts && _jsx(CountTable, { values: question.counts }), question.distribution && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "page-sub", children: ["Average: ", question.average === null ? '—' : question.average.toFixed(2)] }), _jsx(CountTable, { values: question.distribution })] })), question.kind === 'text' && (_jsxs("p", { className: "page-sub", children: [question.responseCount ?? 0, " free-text responses"] }))] }, question.id))) })] }), _jsxs("section", { "aria-labelledby": "survey-free-text", style: { marginTop: 24 }, children: [_jsx("h2", { className: "page-title", id: "survey-free-text", style: { fontSize: 22 }, children: "Free-text responses" }), data.questions
|
|
66
|
+
.filter((question) => question.kind === 'text')
|
|
67
|
+
.map((question) => (_jsxs("div", { className: "card", style: { marginTop: 12 }, children: [_jsx("h3", { className: "card-title", children: question.prompt }), (freeTextByQuestion.get(question.id) ?? []).length === 0 ? (_jsx("p", { className: "page-sub", children: "No free-text responses." })) : (_jsx("ul", { children: (freeTextByQuestion.get(question.id) ?? []).map((entry) => (_jsx("li", { children: entry.value }, `${entry.responseId}:${entry.submittedAt}`))) }))] }, question.id)))] })] }));
|
|
68
|
+
}
|
|
69
|
+
async function readSurveyErrorCode(response) {
|
|
70
|
+
const headerCode = response.headers.get(SURVEY_ERROR_CODE_HEADER);
|
|
71
|
+
if (!isSurveyErrorCode(headerCode))
|
|
72
|
+
return null;
|
|
73
|
+
if (headerCode === 'survey_forbidden' && (response.status === 401 || response.status === 403)) {
|
|
74
|
+
return headerCode;
|
|
75
|
+
}
|
|
76
|
+
const body = await response.json().catch(() => null);
|
|
77
|
+
if (!isPlainRecord(body) || Object.keys(body).length !== 2)
|
|
78
|
+
return null;
|
|
79
|
+
if (!isSurveyErrorCode(body.code) || body.code !== headerCode)
|
|
80
|
+
return null;
|
|
81
|
+
return body.error === SURVEY_ERROR_DEFINITIONS[body.code].error ? body.code : null;
|
|
82
|
+
}
|
|
83
|
+
function isPlainRecord(value) {
|
|
84
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
function StatusPage({ title, detail, retry, }) {
|
|
87
|
+
return (_jsxs("div", { className: "page", role: "status", children: [_jsx("h1", { className: "page-title", children: "Survey responses" }), _jsx("p", { className: "page-sub", children: title }), detail && _jsx("p", { className: "page-sub", children: detail }), retry && (_jsx("button", { type: "button", className: "btn", onClick: retry, children: "Retry" }))] }));
|
|
88
|
+
}
|
|
89
|
+
function CountTable({ values }) {
|
|
90
|
+
return (_jsx("table", { className: "tbl", children: _jsx("tbody", { children: Object.entries(values).map(([label, count]) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: label }), _jsx("td", { className: "tnum", children: count })] }, label))) }) }));
|
|
91
|
+
}
|
|
92
|
+
function formatTime(value) {
|
|
93
|
+
const date = new Date(value);
|
|
94
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
95
|
+
}
|
|
96
|
+
function segmentLabel(value) {
|
|
97
|
+
if (value === 'member')
|
|
98
|
+
return 'Member';
|
|
99
|
+
if (value === 'non_member')
|
|
100
|
+
return 'Non-member';
|
|
101
|
+
return 'Unknown';
|
|
102
|
+
}
|
|
103
|
+
function questionAudienceLabel(value) {
|
|
104
|
+
if (value === 'member')
|
|
105
|
+
return 'Members only';
|
|
106
|
+
if (value === 'non_member')
|
|
107
|
+
return 'Non-members only';
|
|
108
|
+
return 'All respondents';
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=SurveyResponsesScreen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SurveyResponsesScreen.js","sourceRoot":"","sources":["../../../src/ui/screens/SurveyResponsesScreen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ScreenRegistry } from '../components/CmsApp.js';
|
|
2
|
-
export declare function createCmsScreens({ allowInlineAuthorCreation, }?: {
|
|
2
|
+
export declare function createCmsScreens({ allowInlineAuthorCreation, surveyId, }?: {
|
|
3
3
|
allowInlineAuthorCreation?: boolean;
|
|
4
|
+
surveyId?: string;
|
|
4
5
|
}): ScreenRegistry;
|
|
5
6
|
export declare const cmsScreens: ScreenRegistry;
|
|
6
7
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AA4B7D,wBAAgB,gBAAgB,CAAC,EAChC,yBAAgC,EAChC,QAAQ,GACR,GAAE;IACF,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAA;CACZ,GAAG,cAAc,CAmBtB;AAED,eAAO,MAAM,UAAU,EAAE,cAAmC,CAAA"}
|
|
@@ -10,6 +10,7 @@ import { MediaScreen } from './MediaScreen.js';
|
|
|
10
10
|
import { SettingsScreen } from './SettingsScreen.js';
|
|
11
11
|
import { SocialShareScreen } from './SocialShareScreen.js';
|
|
12
12
|
import { SubscriptionsScreen } from './SubscriptionsScreen.js';
|
|
13
|
+
import { SurveyResponsesScreen } from './SurveyResponsesScreen.js';
|
|
13
14
|
import { TopicsScreen } from './TopicsScreen.js';
|
|
14
15
|
// Thin wrappers that supply the openDoc seam for screens that need it.
|
|
15
16
|
// On click, we navigate to the content route; the ContentRoute will handle
|
|
@@ -22,7 +23,7 @@ function CalendarRoute() {
|
|
|
22
23
|
const { navigate } = useHashRouter();
|
|
23
24
|
return _jsx(CalendarScreen, { openDoc: () => navigate('content') });
|
|
24
25
|
}
|
|
25
|
-
export function createCmsScreens({ allowInlineAuthorCreation = true, } = {}) {
|
|
26
|
+
export function createCmsScreens({ allowInlineAuthorCreation = true, surveyId, } = {}) {
|
|
26
27
|
return {
|
|
27
28
|
content: () => _jsx(ContentRoute, { allowInlineAuthorCreation: allowInlineAuthorCreation }),
|
|
28
29
|
'content-insights': () => _jsx(ContentInsightsScreen, {}),
|
|
@@ -33,6 +34,9 @@ export function createCmsScreens({ allowInlineAuthorCreation = true, } = {}) {
|
|
|
33
34
|
authors: () => _jsx(AuthorsScreen, {}),
|
|
34
35
|
topics: () => _jsx(TopicsScreen, {}),
|
|
35
36
|
subscriptions: () => _jsx(SubscriptionsScreen, {}),
|
|
37
|
+
...(surveyId
|
|
38
|
+
? { 'survey-responses': () => _jsx(SurveyResponsesScreen, { surveyId: surveyId }) }
|
|
39
|
+
: {}),
|
|
36
40
|
'social-share': () => (_jsx(SocialShareScreen, { allowInlineAuthorCreation: allowInlineAuthorCreation })),
|
|
37
41
|
settings: () => _jsx(SettingsScreen, {}),
|
|
38
42
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":";AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,uEAAuE;AACvE,2EAA2E;AAC3E,+EAA+E;AAC/E,SAAS,cAAc;IACtB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,eAAe,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC/D,CAAC;AAED,SAAS,aAAa;IACrB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAChC,yBAAyB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":";AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,uEAAuE;AACvE,2EAA2E;AAC3E,+EAA+E;AAC/E,SAAS,cAAc;IACtB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,eAAe,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC/D,CAAC;AAED,SAAS,aAAa;IACrB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAChC,yBAAyB,GAAG,IAAI,EAChC,QAAQ,MAIL,EAAE;IACL,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,IAAC,yBAAyB,EAAE,yBAAyB,GAAI;QACrF,kBAAkB,EAAE,GAAG,EAAE,CAAC,KAAC,qBAAqB,KAAG;QACnD,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;QACnC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QACjC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAC,WAAW,KAAG;QAC5B,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,eAAe,KAAG;QACpC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QAChC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,KAAG;QAC9B,aAAa,EAAE,GAAG,EAAE,CAAC,KAAC,mBAAmB,KAAG;QAC5C,GAAG,CAAC,QAAQ;YACX,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,CAAC,KAAC,qBAAqB,IAAC,QAAQ,EAAE,QAAQ,GAAI,EAAE;YAC7E,CAAC,CAAC,EAAE,CAAC;QACN,cAAc,EAAE,GAAG,EAAE,CAAC,CACrB,KAAC,iBAAiB,IAAC,yBAAyB,EAAE,yBAAyB,GAAI,CAC3E;QACD,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;KAClC,CAAA;AACF,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAmB,gBAAgB,EAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { D1Database } from './d1.js'
|
|
2
|
+
|
|
3
|
+
export interface DekFailureNotificationInput {
|
|
4
|
+
db: D1Database
|
|
5
|
+
workspaceId: string
|
|
6
|
+
contentId: string
|
|
7
|
+
title: string
|
|
8
|
+
reason: string
|
|
9
|
+
userId?: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create unread, in-app notifications when publication ships without a dek.
|
|
14
|
+
* Interactive publishes target the actor; background publishes target every
|
|
15
|
+
* editorial member because no actor is present to receive the failure.
|
|
16
|
+
*/
|
|
17
|
+
export async function createDekFailureNotifications(
|
|
18
|
+
input: DekFailureNotificationInput,
|
|
19
|
+
): Promise<number> {
|
|
20
|
+
let userIds: string[]
|
|
21
|
+
if (input.userId) {
|
|
22
|
+
userIds = [input.userId]
|
|
23
|
+
} else {
|
|
24
|
+
const rows = await input.db
|
|
25
|
+
.prepare(
|
|
26
|
+
`SELECT user_id
|
|
27
|
+
FROM workspace_memberships
|
|
28
|
+
WHERE workspace_id = ?
|
|
29
|
+
AND role IN ('owner', 'senior_editor', 'editor')
|
|
30
|
+
ORDER BY user_id ASC`,
|
|
31
|
+
)
|
|
32
|
+
.bind(input.workspaceId)
|
|
33
|
+
.all<{ user_id: string }>()
|
|
34
|
+
userIds = (rows.results ?? []).map((row) => row.user_id)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const reason = input.reason.trim().slice(0, 160) || 'unknown error'
|
|
38
|
+
const body = `Content was published without a subheading: “${input.title}”. AI generation failed (${reason}). Open the item to add one manually.`
|
|
39
|
+
for (const userId of [...new Set(userIds)]) {
|
|
40
|
+
await input.db
|
|
41
|
+
.prepare(
|
|
42
|
+
`INSERT INTO cms_notifications
|
|
43
|
+
(id, workspace_id, user_id, kind, body, created_at)
|
|
44
|
+
VALUES (?, ?, ?, 'ai_dek_failed', ?, unixepoch())`,
|
|
45
|
+
)
|
|
46
|
+
.bind(crypto.randomUUID(), input.workspaceId, userId, body)
|
|
47
|
+
.run()
|
|
48
|
+
}
|
|
49
|
+
return new Set(userIds).size
|
|
50
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { LlmDispatch } from '../providers/types.js'
|
|
2
|
+
import { dekPrompt } from './ai-prompts.js'
|
|
3
|
+
import { applyAiWrite, parseLockedFields } from './ai-writeback.js'
|
|
4
|
+
import type { D1Database } from './d1.js'
|
|
5
|
+
import { type ContentItemRecord, getContentItem } from './publisher.js'
|
|
6
|
+
|
|
7
|
+
export const DEK_EXEMPLAR_LIMIT = 6
|
|
8
|
+
|
|
9
|
+
export interface DekGenerationContext {
|
|
10
|
+
item: ContentItemRecord
|
|
11
|
+
source: string
|
|
12
|
+
exemplars: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type GenerateContentDekResult =
|
|
16
|
+
| { status: 'generated'; dek: string; written: true }
|
|
17
|
+
| { status: 'locked'; dek: string; written: false }
|
|
18
|
+
| { status: 'unavailable'; dek: null; written: false; reason: string }
|
|
19
|
+
| { status: 'not_found'; dek: null; written: false; reason: 'not_found' }
|
|
20
|
+
|
|
21
|
+
async function loadTypeSource(db: D1Database, item: ContentItemRecord): Promise<string> {
|
|
22
|
+
if (item.type === 'video') {
|
|
23
|
+
const row = await db
|
|
24
|
+
.prepare('SELECT script FROM video_content WHERE content_id = ? LIMIT 1')
|
|
25
|
+
.bind(item.id)
|
|
26
|
+
.first<{ script: string | null }>()
|
|
27
|
+
return row?.script?.trim() ?? ''
|
|
28
|
+
}
|
|
29
|
+
if (item.type === 'podcast') {
|
|
30
|
+
const row = await db
|
|
31
|
+
.prepare('SELECT transcript FROM podcast_content WHERE content_id = ? LIMIT 1')
|
|
32
|
+
.bind(item.id)
|
|
33
|
+
.first<{ transcript: string | null }>()
|
|
34
|
+
return row?.transcript?.trim() ?? ''
|
|
35
|
+
}
|
|
36
|
+
const row = await db
|
|
37
|
+
.prepare(
|
|
38
|
+
`SELECT body_markdown, body_html, subtitle
|
|
39
|
+
FROM article_content WHERE content_id = ? LIMIT 1`,
|
|
40
|
+
)
|
|
41
|
+
.bind(item.id)
|
|
42
|
+
.first<{
|
|
43
|
+
body_markdown: string | null
|
|
44
|
+
body_html: string | null
|
|
45
|
+
subtitle: string | null
|
|
46
|
+
}>()
|
|
47
|
+
return (row?.body_markdown || row?.body_html || row?.subtitle || '').trim()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function loadDekGenerationContext(
|
|
51
|
+
db: D1Database,
|
|
52
|
+
contentId: string,
|
|
53
|
+
): Promise<DekGenerationContext | null> {
|
|
54
|
+
const item = await getContentItem(db, contentId)
|
|
55
|
+
if (!item) return null
|
|
56
|
+
|
|
57
|
+
const typeSource = await loadTypeSource(db, item)
|
|
58
|
+
const description = item.description?.trim() ?? ''
|
|
59
|
+
const source = [description, typeSource].filter(Boolean).join('\n\n')
|
|
60
|
+
const rows = await db
|
|
61
|
+
.prepare(
|
|
62
|
+
`SELECT title, excerpt
|
|
63
|
+
FROM content_items
|
|
64
|
+
WHERE status = 'published'
|
|
65
|
+
AND deleted_at IS NULL
|
|
66
|
+
AND id <> ?
|
|
67
|
+
AND excerpt IS NOT NULL
|
|
68
|
+
AND trim(excerpt) <> ''
|
|
69
|
+
ORDER BY CASE WHEN type = ? THEN 0 ELSE 1 END,
|
|
70
|
+
COALESCE(published_at, 0) DESC,
|
|
71
|
+
id ASC
|
|
72
|
+
LIMIT ?`,
|
|
73
|
+
)
|
|
74
|
+
.bind(contentId, item.type, DEK_EXEMPLAR_LIMIT)
|
|
75
|
+
.all<{ title: string; excerpt: string }>()
|
|
76
|
+
const exemplars = (rows.results ?? []).map(
|
|
77
|
+
(row) => `Headline: ${row.title}\nDek: ${row.excerpt.trim()}`,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return { item, source, exemplars }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeDek(text: string): string {
|
|
84
|
+
return text
|
|
85
|
+
.trim()
|
|
86
|
+
.replace(/^(["“])|(["”])$/g, '')
|
|
87
|
+
.replace(/\s+/g, ' ')
|
|
88
|
+
.trim()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function generateContentDek(input: {
|
|
92
|
+
db: D1Database
|
|
93
|
+
contentId: string
|
|
94
|
+
llm: LlmDispatch
|
|
95
|
+
}): Promise<GenerateContentDekResult> {
|
|
96
|
+
const context = await loadDekGenerationContext(input.db, input.contentId)
|
|
97
|
+
if (!context) {
|
|
98
|
+
return { status: 'not_found', dek: null, written: false, reason: 'not_found' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await input.llm.dispatch({
|
|
102
|
+
...dekPrompt({
|
|
103
|
+
title: context.item.title,
|
|
104
|
+
source: context.source,
|
|
105
|
+
exemplars: context.exemplars,
|
|
106
|
+
}),
|
|
107
|
+
stream: false,
|
|
108
|
+
})
|
|
109
|
+
if (result instanceof ReadableStream) {
|
|
110
|
+
return { status: 'unavailable', dek: null, written: false, reason: 'stream_result' }
|
|
111
|
+
}
|
|
112
|
+
if (result.status === 'unavailable') {
|
|
113
|
+
return {
|
|
114
|
+
status: 'unavailable',
|
|
115
|
+
dek: null,
|
|
116
|
+
written: false,
|
|
117
|
+
reason: result.reason ?? 'unavailable',
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dek = normalizeDek(result.text)
|
|
122
|
+
if (!dek) {
|
|
123
|
+
return { status: 'unavailable', dek: null, written: false, reason: 'empty_result' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const write = applyAiWrite({
|
|
127
|
+
locked: parseLockedFields(context.item.ai_locked_fields),
|
|
128
|
+
field: 'excerpt',
|
|
129
|
+
value: dek,
|
|
130
|
+
})
|
|
131
|
+
if (!write.written) return { status: 'locked', dek, written: false }
|
|
132
|
+
|
|
133
|
+
await input.db
|
|
134
|
+
.prepare('UPDATE content_items SET excerpt = ?, updated_at = unixepoch() WHERE id = ?')
|
|
135
|
+
.bind(dek, input.contentId)
|
|
136
|
+
.run()
|
|
137
|
+
return { status: 'generated', dek, written: true }
|
|
138
|
+
}
|
package/src/engine/ai-prompts.ts
CHANGED
|
@@ -47,12 +47,37 @@ export interface MetaDescriptionPromptInput {
|
|
|
47
47
|
body: string
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Input to dekPrompt. A dek is display copy, not SEO metadata. */
|
|
51
|
+
export interface DekPromptInput {
|
|
52
|
+
title: string
|
|
53
|
+
source: string
|
|
54
|
+
exemplars?: string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** The typed prompt contract shape consumed by LlmDispatch.dispatch(). */
|
|
51
58
|
export interface AiPromptContract {
|
|
52
59
|
task: string
|
|
53
60
|
prompt: string
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Build the prompt for a standfirst-style display subheading (dek).
|
|
65
|
+
* This deliberately differs from the SEO meta-description task: it tells the
|
|
66
|
+
* reader what the piece argues, reveals, or shows in the publication's voice.
|
|
67
|
+
*/
|
|
68
|
+
export function dekPrompt(input: DekPromptInput): AiPromptContract {
|
|
69
|
+
const exemplarsBlock = buildExemplarsBlock(input.exemplars)
|
|
70
|
+
const prompt = `${HOUSE_STYLE_SYSTEM}${exemplarsBlock}
|
|
71
|
+
|
|
72
|
+
Task: Write one standfirst-style subheading (dek) for this piece. It must be 1–2 sentences, roughly 120–200 characters, and reader-facing. State what the piece argues, reveals, or shows. It must not repeat or lightly rephrase the headline. Avoid clickbait and never use the boilerplate shape “with context on the stakes for…”. Return only the dek text with no label, quotation marks, commentary, or preamble.
|
|
73
|
+
|
|
74
|
+
Headline: ${input.title}
|
|
75
|
+
|
|
76
|
+
Source material:
|
|
77
|
+
${input.source}`
|
|
78
|
+
return { task: 'cms.dek', prompt }
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
/** Build the exemplars block when exemplars are supplied. */
|
|
57
82
|
function buildExemplarsBlock(exemplars: string[] | undefined): string {
|
|
58
83
|
if (!exemplars || exemplars.length === 0) return ''
|
package/src/engine/index.ts
CHANGED
|
@@ -5,9 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
// Activity log write helper — first-party D1, cms_activity_log (P5 Task 3).
|
|
7
7
|
export { type ActivityEntry, logActivity } from './activity-log.js'
|
|
8
|
+
// Display-subheading generation with deterministic house exemplars.
|
|
9
|
+
export {
|
|
10
|
+
DEK_EXEMPLAR_LIMIT,
|
|
11
|
+
type DekGenerationContext,
|
|
12
|
+
type GenerateContentDekResult,
|
|
13
|
+
generateContentDek,
|
|
14
|
+
loadDekGenerationContext,
|
|
15
|
+
} from './ai-dek.js'
|
|
16
|
+
export {
|
|
17
|
+
createDekFailureNotifications,
|
|
18
|
+
type DekFailureNotificationInput,
|
|
19
|
+
} from './ai-dek-notifications.js'
|
|
8
20
|
// AI prompt contracts + house-style system prompt (P4 Task 1).
|
|
9
21
|
export {
|
|
10
22
|
type AiPromptContract,
|
|
23
|
+
type DekPromptInput,
|
|
24
|
+
dekPrompt,
|
|
11
25
|
type HeadlinesPromptInput,
|
|
12
26
|
HOUSE_STYLE_SYSTEM,
|
|
13
27
|
headlinesPrompt,
|
package/src/index.ts
CHANGED
|
@@ -34,10 +34,8 @@ export * from './engine/index.js'
|
|
|
34
34
|
|
|
35
35
|
// Injected-provider interfaces + null implementations (spec §3.3).
|
|
36
36
|
export * from './providers/index.js'
|
|
37
|
-
|
|
38
37
|
// Route-layer user types (exported at root for convenience; full routes surface at @growth-labs/cms/routes).
|
|
39
38
|
export type { CmsUser } from './routes/context.js'
|
|
40
|
-
|
|
41
39
|
export {
|
|
42
40
|
type ArticleContentRow,
|
|
43
41
|
type AuthorRow,
|
|
@@ -82,3 +80,5 @@ export {
|
|
|
82
80
|
type WorkspaceMembershipRow,
|
|
83
81
|
type WorkspaceSettingsRow,
|
|
84
82
|
} from './schema/index.js'
|
|
83
|
+
export * from './surveys/errors.js'
|
|
84
|
+
export * from './surveys/schema.js'
|
package/src/integration/index.ts
CHANGED
|
@@ -121,6 +121,10 @@ export default function growthLabsCms(userOptions: CmsIntegrationOptions): Astro
|
|
|
121
121
|
entrypoint: resolveEntrypoint('../ui/api/content/[id]'),
|
|
122
122
|
})
|
|
123
123
|
// AI sub-routes
|
|
124
|
+
injectRoute({
|
|
125
|
+
pattern: `${base}/api/content/[id]/ai/dek`,
|
|
126
|
+
entrypoint: resolveEntrypoint('../ui/api/content/[id]/ai/dek'),
|
|
127
|
+
})
|
|
124
128
|
injectRoute({
|
|
125
129
|
pattern: `${base}/api/content/[id]/ai/headlines`,
|
|
126
130
|
entrypoint: resolveEntrypoint('../ui/api/content/[id]/ai/headlines'),
|
|
@@ -229,6 +233,15 @@ export default function growthLabsCms(userOptions: CmsIntegrationOptions): Astro
|
|
|
229
233
|
pattern: `${base}/api/analytics`,
|
|
230
234
|
entrypoint: resolveEntrypoint('../ui/api/analytics'),
|
|
231
235
|
})
|
|
236
|
+
// Survey reporting — protected JSON plus CSV, backed by the optional host provider.
|
|
237
|
+
injectRoute({
|
|
238
|
+
pattern: `${base}/api/surveys/[surveyId]`,
|
|
239
|
+
entrypoint: resolveEntrypoint('../ui/api/surveys/[surveyId]'),
|
|
240
|
+
})
|
|
241
|
+
injectRoute({
|
|
242
|
+
pattern: `${base}/api/surveys/[surveyId]/responses.csv`,
|
|
243
|
+
entrypoint: resolveEntrypoint('../ui/api/surveys/[surveyId]/responses.csv'),
|
|
244
|
+
})
|
|
232
245
|
// Calendar
|
|
233
246
|
injectRoute({
|
|
234
247
|
pattern: `${base}/api/calendar`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { SurveyIdSchema } from '../surveys/schema.js'
|
|
2
3
|
|
|
3
4
|
const workspaceSchema = z.object({
|
|
4
5
|
id: z.string(),
|
|
@@ -22,6 +23,12 @@ const socialSharingSchema = z
|
|
|
22
23
|
})
|
|
23
24
|
.default({})
|
|
24
25
|
|
|
26
|
+
const surveyResultsSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
surveyId: SurveyIdSchema,
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
|
|
25
32
|
export const cmsIntegrationOptionsSchema = z
|
|
26
33
|
.object({
|
|
27
34
|
workspaces: z.array(workspaceSchema).min(1, 'at least one workspace is required'),
|
|
@@ -30,6 +37,7 @@ export const cmsIntegrationOptionsSchema = z
|
|
|
30
37
|
searchEnabled: z.boolean().default(true),
|
|
31
38
|
theme: themeSchema,
|
|
32
39
|
socialSharing: socialSharingSchema,
|
|
40
|
+
surveyResults: surveyResultsSchema.optional(),
|
|
33
41
|
})
|
|
34
42
|
.transform((o) => ({ ...o, activeWorkspaceId: o.activeWorkspaceId ?? o.workspaces[0].id }))
|
|
35
43
|
|
package/src/providers/null.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SeoProvider,
|
|
8
8
|
SettingsProvider,
|
|
9
9
|
SubscriptionsProvider,
|
|
10
|
+
SurveyProvider,
|
|
10
11
|
} from './types.js'
|
|
11
12
|
import { ProviderHttpError } from './types.js'
|
|
12
13
|
|
|
@@ -179,6 +180,23 @@ export const nullSettings: SettingsProvider = {
|
|
|
179
180
|
},
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
export const nullSurveys: SurveyProvider = {
|
|
184
|
+
async getResults() {
|
|
185
|
+
throw new ProviderHttpError(
|
|
186
|
+
503,
|
|
187
|
+
'survey_provider_unavailable',
|
|
188
|
+
'Survey provider not configured',
|
|
189
|
+
)
|
|
190
|
+
},
|
|
191
|
+
async getCsv() {
|
|
192
|
+
throw new ProviderHttpError(
|
|
193
|
+
503,
|
|
194
|
+
'survey_provider_unavailable',
|
|
195
|
+
'Survey provider not configured',
|
|
196
|
+
)
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
export const nullProviders: CmsProviders = {
|
|
183
201
|
analytics: nullAnalytics,
|
|
184
202
|
subscriptions: nullSubscriptions,
|
|
@@ -186,4 +204,5 @@ export const nullProviders: CmsProviders = {
|
|
|
186
204
|
seo: defaultSeo,
|
|
187
205
|
search: nullSearch,
|
|
188
206
|
settings: nullSettings,
|
|
207
|
+
surveys: nullSurveys,
|
|
189
208
|
}
|