@jini-ai/chat 0.5.3 → 0.5.5
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 +11 -0
- package/dist/core/ag-ui/entry.d.ts +33 -0
- package/dist/core/ag-ui/entry.js +80 -0
- package/dist/core/agentic/chat-capabilities.d.ts +2 -4
- package/dist/core/agentic/index.d.ts +4 -17
- package/dist/core/agentic/index.js +4 -17
- package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
- package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
- package/dist/core/assistant-client/ag-ui-transport.js +147 -0
- package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
- package/dist/core/assistant-client/attachment-liveness.js +93 -0
- package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
- package/dist/core/assistant-client/durable-run-subscription.js +96 -0
- package/dist/core/assistant-client/entry.d.ts +13 -0
- package/dist/core/assistant-client/entry.js +7 -0
- package/dist/core/assistant-client/messages.d.ts +73 -0
- package/dist/core/assistant-client/messages.js +68 -0
- package/dist/core/assistant-client/transport.d.ts +164 -0
- package/dist/core/assistant-client/transport.js +734 -0
- package/dist/core/browser/page-actions.d.ts +121 -0
- package/dist/core/browser/page-actions.js +113 -0
- package/dist/core/composer-history.d.ts +54 -0
- package/dist/core/composer-history.js +57 -0
- package/dist/core/durable-projection.d.ts +28 -0
- package/dist/core/durable-projection.js +71 -0
- package/dist/core/durable-runs/continuation.d.ts +18 -0
- package/dist/core/durable-runs/continuation.js +61 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
- package/dist/core/durable-runs/ledger-contracts.js +2 -0
- package/dist/core/durable-runs/policies.d.ts +6 -0
- package/dist/core/durable-runs/policies.js +6 -0
- package/dist/core/durable-runs/ports.d.ts +123 -0
- package/dist/core/durable-runs/ports.js +2 -0
- package/dist/core/durable-runs/run-contracts.d.ts +17 -0
- package/dist/core/durable-runs/run-contracts.js +2 -0
- package/dist/core/durable-runs/run-lock.d.ts +4 -0
- package/dist/core/durable-runs/run-lock.js +4 -0
- package/dist/core/index.d.ts +9 -19
- package/dist/core/index.js +8 -19
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/question-form/format.d.ts +0 -9
- package/dist/core/question-form/format.js +16 -2
- package/dist/core/question-form/normalize.js +1 -0
- package/dist/core/question-form/secret-answers.d.ts +38 -0
- package/dist/core/question-form/secret-answers.js +45 -0
- package/dist/core/question-form/types.d.ts +2 -0
- package/dist/core/question-form.d.ts +1 -0
- package/dist/core/question-form.js +1 -0
- package/dist/core/run-events/entry.d.ts +208 -16
- package/dist/core/run-events/entry.js +273 -39
- package/dist/core/transcript.d.ts +3 -2
- package/dist/core/transcript.js +19 -3
- package/dist/core/transport.d.ts +4 -0
- package/dist/core/user-text-redaction.d.ts +56 -0
- package/dist/core/user-text-redaction.js +89 -0
- package/dist/embed-widget/browser/page-actions.d.ts +1 -120
- package/dist/embed-widget/browser/page-actions.js +2 -112
- package/dist/embed-widget/react/embed.d.ts +1 -1
- package/dist/embed-widget/react/embed.js +1 -1
- package/dist/react/artifact-types.d.ts +9 -15
- package/dist/react/components/Composer.js +6 -0
- package/dist/react/components/MessageRow.js +24 -11
- package/dist/react/components/QuestionForm.d.ts +2 -1
- package/dist/react/components/QuestionForm.js +18 -12
- package/dist/react/components/QuestionsPanel.d.ts +3 -2
- package/dist/react/components/QuestionsPanel.js +2 -2
- package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
- package/dist/react/features/chat-pane/agent-tools.js +0 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +9 -4
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +7 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +51 -0
- package/dist/react/features/chat-pane/rules.d.ts +3 -2
- package/dist/react/features/chat-pane/rules.js +12 -4
- package/dist/react/features/chat-pane/types.d.ts +30 -1
- package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
- package/dist/react/hooks/assistant-chats-contracts.js +2 -0
- package/dist/react/hooks/assistant-chats.d.ts +7 -0
- package/dist/react/hooks/assistant-chats.js +960 -0
- package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
- package/dist/react/hooks/attachment-preview-cache.js +1 -1
- package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
- package/dist/react/hooks/composer-draft-cache.js +2 -2
- package/dist/react/hooks/composer-history-storage.d.ts +8 -0
- package/dist/react/hooks/composer-history-storage.js +40 -0
- package/dist/react/hooks/fab-position.d.ts +90 -0
- package/dist/react/hooks/fab-position.js +271 -0
- package/dist/react/hooks/useComposer.d.ts +10 -1
- package/dist/react/hooks/useComposer.js +14 -2
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
- package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
- package/dist/react/hooks/useConversation.d.ts +4 -1
- package/dist/react/hooks/useConversation.js +20 -14
- package/dist/react/hooks/useRunStream.js +12 -2
- package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
- package/dist/react/hooks/useUserTextGuard.js +19 -0
- package/dist/react/index.d.ts +14 -21
- package/dist/react/index.js +8 -0
- package/dist/server/durable-runs/accept.d.ts +25 -0
- package/dist/server/durable-runs/accept.js +21 -0
- package/dist/server/durable-runs/entry.d.ts +5 -0
- package/dist/server/durable-runs/entry.js +4 -0
- package/dist/server/durable-runs/recover.d.ts +5 -0
- package/dist/server/durable-runs/recover.js +147 -0
- package/dist/server/run-finalizer/ports.d.ts +2 -16
- package/dist/store/legacy/sqlite/messages.d.ts +24 -18
- package/dist/store/legacy/sqlite/messages.js +12 -2
- package/dist/store/pglite/index.d.ts +2 -1
- package/dist/store/postgres/index.d.ts +2 -1
- package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
- package/dist/store/runtime/chat-expiry-sweep.js +70 -0
- package/dist/store/runtime/chat-history-store.d.ts +20 -0
- package/dist/store/runtime/chat-history-store.js +50 -0
- package/dist/store/runtime/durable-run-store.d.ts +21 -0
- package/dist/store/runtime/durable-run-store.js +342 -0
- package/dist/store/runtime/entry.d.ts +7 -0
- package/dist/store/runtime/entry.js +6 -0
- package/dist/store/runtime/kernel.d.ts +29 -0
- package/dist/store/runtime/kernel.js +2 -0
- package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
- package/dist/store/runtime/run-guarded-chat-store.js +42 -0
- package/dist/store/runtime/run-ledger.d.ts +15 -0
- package/dist/store/runtime/run-ledger.js +206 -0
- package/dist/store/sql/boundary.d.ts +6 -0
- package/dist/store/sql/boundary.js +7 -1
- package/dist/store/sql/store.d.ts +3 -2
- package/dist/store/sql/store.js +9 -2
- package/dist/store/sqlite/index.d.ts +3 -2
- package/package.json +75 -10
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** One credential policy for chat text; secure card values must bypass this text-only boundary. */
|
|
2
|
+
import { redactSecretShapes } from '@jini-ai/diagnostics/redaction/secrets-only';
|
|
3
|
+
export const SECRET_REDACTED_NOTICE = 'I removed what looked like a key from your message. If it was real, rotate it. Use the secure form to save it.';
|
|
4
|
+
export const CREDENTIAL_CARD_GUIDANCE = 'Use the secure form to save keys, tokens, passwords, and secrets. Chat answers are sent to the assistant.';
|
|
5
|
+
const REMOVED = '[token removed]';
|
|
6
|
+
const INTERNAL_REMOVED = '[REDACTED:chat_secret]';
|
|
7
|
+
const REDACTION_MARKER = /\[REDACTED:[^\]\r\n]+\]/g;
|
|
8
|
+
const CHAT_RULES = [
|
|
9
|
+
{ kind: 'chat_key', pattern: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{16,}/g, replacement: () => INTERNAL_REMOVED },
|
|
10
|
+
{ kind: 'chat_key', pattern: /(?<![A-Za-z0-9_])xox[baprs]-[A-Za-z0-9-]{16,}/g, replacement: () => INTERNAL_REMOVED },
|
|
11
|
+
// Explicit authorization syntax catches even short values. Exempt scheme descriptions in prose.
|
|
12
|
+
{ kind: 'chat_bearer', pattern: /\bBearer[ \t]+(?!\[REDACTED)(?!(?:token|tokens|authentication|authorization|auth|scheme|credentials?)\b)[A-Za-z0-9._~+\/=-]+/gi, replacement: () => 'Bearer ' + INTERNAL_REMOVED },
|
|
13
|
+
];
|
|
14
|
+
function entropy(text) {
|
|
15
|
+
const counts = new Map();
|
|
16
|
+
for (const char of text)
|
|
17
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
18
|
+
let bits = 0;
|
|
19
|
+
for (const count of counts.values()) {
|
|
20
|
+
const probability = count / text.length;
|
|
21
|
+
bits -= probability * Math.log2(probability);
|
|
22
|
+
}
|
|
23
|
+
return bits;
|
|
24
|
+
}
|
|
25
|
+
/** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
|
|
26
|
+
* hexadecimal hashes, identifiers and prose. Strong credential syntax inside a URL is still caught. */
|
|
27
|
+
export function redactUserText({ text }, { secretField = false } = {}) {
|
|
28
|
+
// Existing removal markers must survive a second pass through assignment rules (api_key=…).
|
|
29
|
+
const literalMarkers = new Map();
|
|
30
|
+
let literalPrefix = 'chat_literal_';
|
|
31
|
+
while (text.includes(literalPrefix))
|
|
32
|
+
literalPrefix += '_';
|
|
33
|
+
const protectedText = text.replace(REDACTION_MARKER, marker => {
|
|
34
|
+
const key = '[REDACTED:' + literalPrefix + literalMarkers.size + ']';
|
|
35
|
+
literalMarkers.set(key, marker);
|
|
36
|
+
return key;
|
|
37
|
+
}).replaceAll(REMOVED, INTERNAL_REMOVED);
|
|
38
|
+
const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
|
|
39
|
+
let count = base.redactions;
|
|
40
|
+
const urls = [...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi)]
|
|
41
|
+
.map(match => ({ start: match.index, end: match.index + match[0].length }));
|
|
42
|
+
let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
|
|
43
|
+
if (urls.some(url => offset >= url.start && offset < url.end))
|
|
44
|
+
return candidate;
|
|
45
|
+
if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
|
|
46
|
+
return candidate;
|
|
47
|
+
const bits = entropy(candidate);
|
|
48
|
+
const mixedAlphabet = /[A-Z]/.test(candidate) && /[a-z]/.test(candidate);
|
|
49
|
+
if (bits < 4 || (!/[0-9]/.test(candidate) && (!mixedAlphabet || bits < 4.5)))
|
|
50
|
+
return candidate;
|
|
51
|
+
count += 1;
|
|
52
|
+
return INTERNAL_REMOVED;
|
|
53
|
+
});
|
|
54
|
+
// A labelled credential answer can be a short password with no recognisable shape at all.
|
|
55
|
+
if (secretField && cleaned.trim() !== '' && cleaned !== INTERNAL_REMOVED) {
|
|
56
|
+
cleaned = INTERNAL_REMOVED;
|
|
57
|
+
count = Math.max(1, count);
|
|
58
|
+
}
|
|
59
|
+
return { text: cleaned.replace(REDACTION_MARKER, marker => literalMarkers.get(marker) ?? REMOVED), secretRedacted: count > 0, count };
|
|
60
|
+
}
|
|
61
|
+
/** Prepare before storage OR model dispatch. Return only sanitized content and non-secret metadata. */
|
|
62
|
+
export function redactUserMessage({ message }, options = {}) {
|
|
63
|
+
if (message.role !== 'user')
|
|
64
|
+
return { message, secretRedacted: false, count: 0 };
|
|
65
|
+
const result = (options.redactUserText ?? redactUserText)({ text: message.content });
|
|
66
|
+
const signal = result.secretRedacted ? { secretRedacted: true, count: result.count } : undefined;
|
|
67
|
+
if (signal)
|
|
68
|
+
options.onSecretRedacted?.(signal);
|
|
69
|
+
return {
|
|
70
|
+
message: { ...message, content: result.text, ...(signal ? { secretRedaction: signal } : {}) },
|
|
71
|
+
secretRedacted: result.secretRedacted, count: result.count,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Also protects replay of historical text accepted before the guard was installed. */
|
|
75
|
+
export function redactUserHistory({ history }, options = {}) {
|
|
76
|
+
return history.map(message => redactUserMessage({ message }, options).message);
|
|
77
|
+
}
|
|
78
|
+
/** Redact model input and append the host's non-secret card guidance once, using the redaction
|
|
79
|
+
* signal rather than display text. `secretRedacted` carries an earlier ingress/composer signal;
|
|
80
|
+
* persistence must continue to use redactUserText/redactUserMessage without the model note. */
|
|
81
|
+
export function guardUserText({ text }, options = {}) {
|
|
82
|
+
const safe = (options.redactUserText ?? redactUserText)({ text }, {});
|
|
83
|
+
if (!safe.secretRedacted && options.secretRedacted !== true)
|
|
84
|
+
return safe.text;
|
|
85
|
+
if (!options.modelNote || safe.text.includes(options.modelNote))
|
|
86
|
+
return safe.text;
|
|
87
|
+
return `${safe.text}\n\n${options.modelNote}`;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=user-text-redaction.js.map
|
|
@@ -1,121 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export interface ResolvedPageTarget {
|
|
3
|
-
readonly slug: string;
|
|
4
|
-
readonly title: string;
|
|
5
|
-
readonly path: string;
|
|
6
|
-
}
|
|
7
|
-
export type PageAction = {
|
|
8
|
-
readonly type: 'navigate';
|
|
9
|
-
readonly target: ResolvedPageTarget;
|
|
10
|
-
readonly auto: boolean;
|
|
11
|
-
} | {
|
|
12
|
-
readonly type: 'scroll_to' | 'highlight';
|
|
13
|
-
readonly target: ResolvedPageTarget;
|
|
14
|
-
};
|
|
15
|
-
export type NavigateAction = Extract<PageAction, {
|
|
16
|
-
type: 'navigate';
|
|
17
|
-
}>;
|
|
18
|
-
export type NonNavigateAction = Exclude<PageAction, {
|
|
19
|
-
type: 'navigate';
|
|
20
|
-
}>;
|
|
21
|
-
export interface DirectiveCarryingEvent {
|
|
22
|
-
readonly kind: string;
|
|
23
|
-
readonly name?: string;
|
|
24
|
-
readonly data?: unknown;
|
|
25
|
-
}
|
|
26
|
-
export interface SplitPageActions {
|
|
27
|
-
readonly navigate: NavigateAction | null;
|
|
28
|
-
readonly other: NonNavigateAction | null;
|
|
29
|
-
}
|
|
30
|
-
/** Narrows an untrusted, bare queued action. URL authorization remains a separate host policy. O(1). */
|
|
31
|
-
export declare function isQueuedPageAction(args: {
|
|
32
|
-
value: unknown;
|
|
33
|
-
}): args is {
|
|
34
|
-
value: PageAction;
|
|
35
|
-
};
|
|
36
|
-
/** Narrows an untrusted directive envelope, dropping unknown kinds. O(1). */
|
|
37
|
-
export declare function isPageActionDirective(args: {
|
|
38
|
-
value: unknown;
|
|
39
|
-
}): args is {
|
|
40
|
-
value: {
|
|
41
|
-
kind: 'page_action';
|
|
42
|
-
action: PageAction;
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
/** Preserves valid action order while ignoring unrelated events. O(events) time and output space. */
|
|
46
|
-
export declare function extractPageActions({ events, directiveEventName }: {
|
|
47
|
-
events: readonly DirectiveCarryingEvent[] | undefined;
|
|
48
|
-
directiveEventName: string;
|
|
49
|
-
}): PageAction[];
|
|
50
|
-
/** Returns only the first navigation and first page treatment. O(actions), O(1) extra space. */
|
|
51
|
-
export declare function splitPageActions({ actions }: {
|
|
52
|
-
actions: readonly PageAction[];
|
|
53
|
-
}): SplitPageActions;
|
|
54
|
-
export interface TargetSelectionPolicy {
|
|
55
|
-
readonly candidateSelector: string;
|
|
56
|
-
readonly excludedAncestorSelector: string;
|
|
57
|
-
}
|
|
58
|
-
/** Exact title match first, case-insensitive fallback; never searches excluded UI. O(candidates). */
|
|
59
|
-
export declare function findTargetElement({ title, root, selection }: {
|
|
60
|
-
title: string;
|
|
61
|
-
root: Pick<ParentNode, 'querySelectorAll'>;
|
|
62
|
-
selection: TargetSelectionPolicy;
|
|
63
|
-
}): Element | null;
|
|
64
|
-
export interface PageActionTimerPort {
|
|
65
|
-
schedule(args: {
|
|
66
|
-
callback: () => void;
|
|
67
|
-
delayMs: number;
|
|
68
|
-
}): unknown;
|
|
69
|
-
cancel(args: {
|
|
70
|
-
handle: unknown;
|
|
71
|
-
}): void;
|
|
72
|
-
}
|
|
73
|
-
export interface PageActionPort {
|
|
74
|
-
allows(args: {
|
|
75
|
-
action: PageAction;
|
|
76
|
-
}): boolean;
|
|
77
|
-
navigate(args: {
|
|
78
|
-
action: NavigateAction;
|
|
79
|
-
}): boolean;
|
|
80
|
-
execute(args: {
|
|
81
|
-
action: NonNavigateAction;
|
|
82
|
-
}): boolean;
|
|
83
|
-
clearHighlight(args: Record<string, never>): void;
|
|
84
|
-
}
|
|
85
|
-
export interface BrowserPageActionController extends PageActionPort {
|
|
86
|
-
applyHighlight(args: {
|
|
87
|
-
element: Element;
|
|
88
|
-
}): void;
|
|
89
|
-
scrollToElement(args: {
|
|
90
|
-
element: Element;
|
|
91
|
-
}): void;
|
|
92
|
-
}
|
|
93
|
-
export interface BrowserPageActionsArgs {
|
|
94
|
-
readonly root: Pick<ParentNode, 'querySelectorAll'>;
|
|
95
|
-
readonly selection: TargetSelectionPolicy;
|
|
96
|
-
readonly allowPath: (args: {
|
|
97
|
-
path: string;
|
|
98
|
-
}) => boolean;
|
|
99
|
-
readonly navigate: (args: {
|
|
100
|
-
path: string;
|
|
101
|
-
}) => void;
|
|
102
|
-
readonly reducedMotion: (args: Record<string, never>) => boolean;
|
|
103
|
-
readonly scroll: (args: {
|
|
104
|
-
element: Element;
|
|
105
|
-
options: ScrollIntoViewOptions;
|
|
106
|
-
}) => void;
|
|
107
|
-
readonly timers: PageActionTimerPort;
|
|
108
|
-
readonly highlightClass: string;
|
|
109
|
-
readonly fadeAnimationName: string;
|
|
110
|
-
readonly cleanupFallbackMs: number;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
|
|
114
|
-
* At most one highlight belongs to this instance; cleanup and navigation remove it.
|
|
115
|
-
* URL policy is checked before queueing, navigation or page effects. Stale timers and descendant
|
|
116
|
-
* animation events cannot remove the current highlight. Navigation paths are resolved by the host,
|
|
117
|
-
* never guessed from a title; each controller owns its mutable highlight state.
|
|
118
|
-
* @complexity Execute O(candidates); other operations O(1), with one timer/listener retained.
|
|
119
|
-
*/
|
|
120
|
-
export declare function createBrowserPageActions(args: BrowserPageActionsArgs): BrowserPageActionController;
|
|
1
|
+
export * from '../../core/browser/page-actions.js';
|
|
121
2
|
//# sourceMappingURL=page-actions.d.ts.map
|
|
@@ -1,113 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
}
|
|
4
|
-
function target(value) {
|
|
5
|
-
return record(value) && typeof value.slug === 'string' && typeof value.title === 'string' && typeof value.path === 'string';
|
|
6
|
-
}
|
|
7
|
-
/** Narrows an untrusted, bare queued action. URL authorization remains a separate host policy. O(1). */
|
|
8
|
-
export function isQueuedPageAction(args) {
|
|
9
|
-
const { value } = args;
|
|
10
|
-
if (!record(value) || !target(value.target))
|
|
11
|
-
return false;
|
|
12
|
-
return value.type === 'navigate' ? typeof value.auto === 'boolean' : value.type === 'scroll_to' || value.type === 'highlight';
|
|
13
|
-
}
|
|
14
|
-
/** Narrows an untrusted directive envelope, dropping unknown kinds. O(1). */
|
|
15
|
-
export function isPageActionDirective(args) {
|
|
16
|
-
const { value } = args;
|
|
17
|
-
return record(value) && value.kind === 'page_action' && isQueuedPageAction({ value: value.action });
|
|
18
|
-
}
|
|
19
|
-
/** Preserves valid action order while ignoring unrelated events. O(events) time and output space. */
|
|
20
|
-
export function extractPageActions({ events, directiveEventName }) {
|
|
21
|
-
const actions = [];
|
|
22
|
-
for (const event of events ?? []) {
|
|
23
|
-
const envelope = { value: event.data };
|
|
24
|
-
if (event.kind === 'ext' && event.name === directiveEventName && isPageActionDirective(envelope)) {
|
|
25
|
-
actions.push(envelope.value.action);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return actions;
|
|
29
|
-
}
|
|
30
|
-
/** Returns only the first navigation and first page treatment. O(actions), O(1) extra space. */
|
|
31
|
-
export function splitPageActions({ actions }) {
|
|
32
|
-
return {
|
|
33
|
-
navigate: actions.find((a) => a.type === 'navigate') ?? null,
|
|
34
|
-
other: actions.find((a) => a.type !== 'navigate') ?? null,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/** Exact title match first, case-insensitive fallback; never searches excluded UI. O(candidates). */
|
|
38
|
-
export function findTargetElement({ title, root, selection }) {
|
|
39
|
-
const needle = title.trim();
|
|
40
|
-
if (!needle)
|
|
41
|
-
return null;
|
|
42
|
-
const candidates = Array.from(root.querySelectorAll(selection.candidateSelector))
|
|
43
|
-
.filter(el => !el.closest(selection.excludedAncestorSelector));
|
|
44
|
-
return candidates.find(el => (el.textContent ?? '').trim() === needle)
|
|
45
|
-
?? candidates.find(el => (el.textContent ?? '').trim().toLowerCase() === needle.toLowerCase()) ?? null;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Creates page effects with injected DOM, navigation, URL policy, scroll and timers.
|
|
49
|
-
* At most one highlight belongs to this instance; cleanup and navigation remove it.
|
|
50
|
-
* URL policy is checked before queueing, navigation or page effects. Stale timers and descendant
|
|
51
|
-
* animation events cannot remove the current highlight. Navigation paths are resolved by the host,
|
|
52
|
-
* never guessed from a title; each controller owns its mutable highlight state.
|
|
53
|
-
* @complexity Execute O(candidates); other operations O(1), with one timer/listener retained.
|
|
54
|
-
*/
|
|
55
|
-
export function createBrowserPageActions(args) {
|
|
56
|
-
if (!Number.isFinite(args.cleanupFallbackMs) || args.cleanupFallbackMs < 0)
|
|
57
|
-
throw new Error('Invalid highlight deadline');
|
|
58
|
-
let current = null;
|
|
59
|
-
const clearHighlight = (_args) => {
|
|
60
|
-
if (!current)
|
|
61
|
-
return;
|
|
62
|
-
const old = current;
|
|
63
|
-
current = null;
|
|
64
|
-
args.timers.cancel({ handle: old.handle });
|
|
65
|
-
old.element.removeEventListener('animationend', old.onEnd);
|
|
66
|
-
old.element.classList.remove(args.highlightClass);
|
|
67
|
-
};
|
|
68
|
-
function highlight(element) {
|
|
69
|
-
if (current?.element === element)
|
|
70
|
-
return;
|
|
71
|
-
clearHighlight({});
|
|
72
|
-
const onEnd = event => {
|
|
73
|
-
if (event.target === element && event.animationName === args.fadeAnimationName)
|
|
74
|
-
clearHighlight({});
|
|
75
|
-
};
|
|
76
|
-
element.classList.add(args.highlightClass);
|
|
77
|
-
element.addEventListener('animationend', onEnd);
|
|
78
|
-
// Identity check makes even a late callback from a canceled timer harmless.
|
|
79
|
-
const handle = args.timers.schedule({ delayMs: args.cleanupFallbackMs, callback: () => {
|
|
80
|
-
if (current?.element === element && current.onEnd === onEnd)
|
|
81
|
-
clearHighlight({});
|
|
82
|
-
} });
|
|
83
|
-
current = { element, handle, onEnd };
|
|
84
|
-
}
|
|
85
|
-
const scrollToElement = ({ element }) => {
|
|
86
|
-
args.scroll({ element, options: { behavior: args.reducedMotion({}) ? 'auto' : 'smooth', block: 'center' } });
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
applyHighlight({ element }) { highlight(element); },
|
|
90
|
-
scrollToElement,
|
|
91
|
-
allows({ action }) { return args.allowPath({ path: action.target.path }); },
|
|
92
|
-
navigate({ action }) {
|
|
93
|
-
if (!args.allowPath({ path: action.target.path }))
|
|
94
|
-
return false;
|
|
95
|
-
clearHighlight({});
|
|
96
|
-
args.navigate({ path: action.target.path });
|
|
97
|
-
return true;
|
|
98
|
-
},
|
|
99
|
-
execute({ action }) {
|
|
100
|
-
if (!args.allowPath({ path: action.target.path }))
|
|
101
|
-
return false;
|
|
102
|
-
const element = findTargetElement({ title: action.target.title, root: args.root, selection: args.selection });
|
|
103
|
-
if (!element)
|
|
104
|
-
return false;
|
|
105
|
-
scrollToElement({ element });
|
|
106
|
-
if (action.type === 'highlight')
|
|
107
|
-
highlight(element);
|
|
108
|
-
return true;
|
|
109
|
-
},
|
|
110
|
-
clearHighlight,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
1
|
+
// Compatibility path; shared page-action decisions belong to chat core.
|
|
2
|
+
export * from '../../core/browser/page-actions.js';
|
|
113
3
|
//# sourceMappingURL=page-actions.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatTransport } from '../../core/transport.js';
|
|
2
2
|
import type { ChatPaneAgent } from '../../react/chat-pane.js';
|
|
3
3
|
import type { BrowserSessionStore } from '../browser/session-store.js';
|
|
4
|
-
import type { PageActionPort } from '
|
|
4
|
+
import type { PageActionPort } from '../../core/browser/page-actions.js';
|
|
5
5
|
import type { EmbedHeaderCopy, EscapeKeyPort } from './header.js';
|
|
6
6
|
export { EmbedChatHeader } from './header.js';
|
|
7
7
|
export type { EmbedChatHeaderArgs, EmbedHeaderCopy, EscapeKeyPort } from './header.js';
|
|
@@ -5,7 +5,7 @@ import { isTerminalRunStatus } from '../../core/messages.js';
|
|
|
5
5
|
import { ChatPane } from '../../react/chat-pane.js';
|
|
6
6
|
import { ChatFab } from '../../react/components/ChatFab.js';
|
|
7
7
|
import { JiniChatProvider } from '../../react/components/JiniChatProvider.js';
|
|
8
|
-
import { extractPageActions, splitPageActions, isQueuedPageAction } from '
|
|
8
|
+
import { extractPageActions, splitPageActions, isQueuedPageAction } from '../../core/browser/page-actions.js';
|
|
9
9
|
import { EmbedChatHeader } from './header.js';
|
|
10
10
|
export { EmbedChatHeader } from './header.js';
|
|
11
11
|
const shellStyles = `
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module artifact-types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* shape r4b §2 specifies, with the intent that a future pass re-points every
|
|
12
|
-
* import below at `@jini-ai/renderers-react`'s real exports once that package
|
|
13
|
-
* lands, deletes this file, and removes the re-export from `index.ts`.
|
|
4
|
+
* Chat's public streaming artifact contracts, used by useArtifactStream and
|
|
5
|
+
* ProjectContextValue. The renderer implementation is available in @jini-ai/ui,
|
|
6
|
+
* but direct delegation would change this compatibility surface: chat registers
|
|
7
|
+
* mutably, resolves newest-first, returns unregister handles, accepts files
|
|
8
|
+
* without manifests and returns the matched file. UI's registry is immutable,
|
|
9
|
+
* replaces registrations by ID, requires a manifest and returns that manifest;
|
|
10
|
+
* its predicate and resolve signatures also pass hints as a separate option.
|
|
14
11
|
*
|
|
15
|
-
* TODO(
|
|
16
|
-
*
|
|
17
|
-
* re-exports) once `@jini-ai/renderers-react` ships real `ArtifactFile`/
|
|
18
|
-
* `ArtifactRenderer`/`RendererRegistry` implementations. Do not block this
|
|
19
|
-
* package's other work on that package landing first.
|
|
12
|
+
* TODO(renderer-delegation): Coordinator must resolve these contracts before
|
|
13
|
+
* delegating to UI. A wildcard re-export is not a behavior-preserving migration.
|
|
20
14
|
*/
|
|
21
15
|
import type { ArtifactManifest } from '../core/index.js';
|
|
22
16
|
/** Generic artifact-file shape a host's project/workspace file maps onto. */
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
16
16
|
import { RemixIcon } from '@jini-ai/ui';
|
|
17
17
|
import { useT } from '../hooks/context.js';
|
|
18
|
+
import { useComposerHistoryKeys } from '../hooks/useComposerHistoryKeys.hooks.js';
|
|
18
19
|
import { AttachmentTray } from './AttachmentTray.js';
|
|
19
20
|
import { ComposerDiscoveryMenu, ComposerSlashMenu } from './ComposerDiscovery.js';
|
|
20
21
|
import { appendComposerDiscovery, composerDiscoveryMenuPosition, composerSlashMenuPosition, filterComposerDiscovery, parseComposerSlashQuery, replaceComposerSlashTrigger, resolveComposerSlashInvocation, resolveComposerSlashKeyAction, } from './composer-discovery.js';
|
|
@@ -103,6 +104,9 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
103
104
|
* point the shrinking list is its own feedback.
|
|
104
105
|
*/
|
|
105
106
|
const showSlashFilterHint = slashQuery !== null && slashQuery.command === '' && slashQuery.argument === null;
|
|
107
|
+
const handleHistoryKey = useComposerHistoryKeys({ composer, textareaRef }, {
|
|
108
|
+
popupOpen: slashOpen || discoveryMenuOpen || workdirOpen || composer.mention.open,
|
|
109
|
+
});
|
|
106
110
|
function restoreComposerFocus() {
|
|
107
111
|
textareaRef.current?.focus();
|
|
108
112
|
}
|
|
@@ -286,6 +290,8 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
286
290
|
return;
|
|
287
291
|
}
|
|
288
292
|
}
|
|
293
|
+
if (handleHistoryKey(event))
|
|
294
|
+
return;
|
|
289
295
|
if (event.key !== 'Enter' || event.shiftKey)
|
|
290
296
|
return;
|
|
291
297
|
event.preventDefault();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
|
|
2
3
|
/**
|
|
3
4
|
* @module MessageRow
|
|
4
5
|
*
|
|
@@ -120,6 +121,23 @@ function UsageSummary({ usage }) {
|
|
|
120
121
|
return null;
|
|
121
122
|
return (_jsxs("div", { className: "jini-message-usage", children: [_jsx("span", { className: "jini-message-usage-dot", "aria-hidden": true, children: "\u25CF" }), t('Done'), " \u00B7 ", parts.join(' · ')] }));
|
|
122
123
|
}
|
|
124
|
+
function messageBlocks({ visibleContent, message, rows }, _optional = {}) {
|
|
125
|
+
// Interleaving is skipped outright when an artifact was stripped. `stripArtifact` operates on the
|
|
126
|
+
// whole string and trims, so the surviving text no longer lines up with the per-run offsets the
|
|
127
|
+
// event walk produces, and an artifact block could legitimately span a tool call. Rather than
|
|
128
|
+
// reason about partial overlaps, an artifact-bearing message keeps the flat layout — those
|
|
129
|
+
// messages are dominated by the artifact panel anyway, so the ordering matters least there.
|
|
130
|
+
if (visibleContent !== message.content)
|
|
131
|
+
return null;
|
|
132
|
+
return interleaveMessageBlocks({ events: message.events, content: message.content, rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }) });
|
|
133
|
+
}
|
|
134
|
+
function messageAwaitingAnswer({ runStreaming, runInProgress, events }, _optional = {}) {
|
|
135
|
+
return runStreaming === true && runInProgress && isAwaitingAnswer({ events });
|
|
136
|
+
}
|
|
137
|
+
function AssistantMessageFooter({ message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent }, _optional = {}) {
|
|
138
|
+
const t = useT();
|
|
139
|
+
return _jsxs(_Fragment, { children: [usageEvent ? _jsx(UsageSummary, { usage: usageEvent }) : null, terminalNotice ? _jsx("div", { className: "jini-message-error", role: "status", children: t(terminalNotice) }) : null, !pending && !runInProgress ? (_jsx("div", { className: "jini-message-actions jini-message-actions--assistant", children: _jsx(CopyMessageButton, { text: visibleContent, label: t('Copy message') }) })) : null, runInProgress ? _jsx(RunActivityLine, { events: message.events, active: pending || (runStreaming === true && runInProgress) }) : null] });
|
|
140
|
+
}
|
|
123
141
|
/**
|
|
124
142
|
* Quiet, icon-only copy affordance shared by the user- and assistant-message
|
|
125
143
|
* action rows below. Delegates the actual clipboard write (and its
|
|
@@ -147,20 +165,14 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
|
|
|
147
165
|
if (message.role === 'user') {
|
|
148
166
|
return (_jsxs("div", { className: "jini-message jini-message-user", "data-message-id": message.id, "data-agent-element": `chat-message-${message.id}`, "data-agent-role": "region", "data-agent-label": "A message from the user", children: [message.attachments && message.attachments.length > 0 ? (_jsx("div", { className: "jini-message-attachments", children: message.attachments.map((a) => (_jsx("button", { type: "button", className: "jini-message-attachment-chip", onClick: () => setOpenAttachment(a), title: t('Open {name}', { name: a.name }), "aria-label": t('Open {name}', { name: a.name }), children: renderAttachment ? renderAttachment(a) : a.name }, a.path))) })) : null, _jsx("div", { className: "jini-message-content", children: message.content }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: message.content, label: t('Copy message') }) }), openAttachment ? (_jsx(AttachmentPreviewModal, { attachment: openAttachment, onClose: () => setOpenAttachment(null) })) : null] }));
|
|
149
167
|
}
|
|
168
|
+
const terminalNotice = terminalMessageNotice({ message }, {});
|
|
150
169
|
const visibleContent = stripArtifact({ content: message.content });
|
|
151
170
|
const segments = splitOnQuestionForms({ input: visibleContent });
|
|
152
171
|
const usageEvent = message.events?.filter((ev) => ev.kind === 'usage').pop();
|
|
153
|
-
|
|
154
|
-
// whole string and trims, so the surviving text no longer lines up with the per-run offsets the
|
|
155
|
-
// event walk produces, and an artifact block could legitimately span a tool call. Rather than
|
|
156
|
-
// reason about partial overlaps, an artifact-bearing message keeps the flat layout — those
|
|
157
|
-
// messages are dominated by the artifact panel anyway, so the ordering matters least there.
|
|
158
|
-
const blocks = visibleContent === message.content
|
|
159
|
-
? interleaveMessageBlocks({ events: message.events, content: message.content, rows: timeline.rows }, { slotOf: (ev) => extEventSlot({ name: ev.name, data: ev.data }) })
|
|
160
|
-
: null;
|
|
172
|
+
const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
|
|
161
173
|
const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
|
|
162
174
|
const runInProgress = isRunInProgress(message.runStatus);
|
|
163
|
-
const awaitingAnswer = runStreaming
|
|
175
|
+
const awaitingAnswer = messageAwaitingAnswer({ runStreaming, runInProgress, events: message.events }, {});
|
|
164
176
|
const renderToolCard = (row) => (_jsx(ToolCard, { use: row.use, result: row.result, runStreaming: runStreaming, runSucceeded: runSucceeded, ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}) }, row.id));
|
|
165
177
|
const renderExtGroup = (group) => {
|
|
166
178
|
const renderer = getExtEventRenderer({ name: group.name });
|
|
@@ -197,7 +209,7 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
|
|
|
197
209
|
return null;
|
|
198
210
|
return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
|
|
199
211
|
})
|
|
200
|
-
: (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })),
|
|
212
|
+
: (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent })] }));
|
|
201
213
|
}
|
|
202
214
|
/**
|
|
203
215
|
* The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
|
|
@@ -205,7 +217,8 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
|
|
|
205
217
|
* Kept a separate component so its one-second tick re-renders only this line, not the whole row.
|
|
206
218
|
*/
|
|
207
219
|
function RunActivityLine({ events, active }) {
|
|
208
|
-
const
|
|
220
|
+
const activityLabel = useRunActivity({ events: events, active: active });
|
|
221
|
+
const label = continuingRunNotice({ events, active }, {}) ?? activityLabel;
|
|
209
222
|
if (label === null)
|
|
210
223
|
return null;
|
|
211
224
|
return (_jsx("div", { className: "jini-message-pending jini-run-activity", "aria-live": "off", children: label }));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type UserTextRedactionOptions } from '../../core/user-text-redaction.js';
|
|
1
2
|
import type { QuestionForm as QuestionFormType } from '../../core/index.js';
|
|
2
3
|
export interface QuestionFormFileSubmission {
|
|
3
4
|
questionId: string;
|
|
@@ -9,7 +10,7 @@ export interface QuestionFormHandle {
|
|
|
9
10
|
/** Submit with no answers — every question is optional, so this records each as "(skipped)" and moves on. */
|
|
10
11
|
skipAll: () => void;
|
|
11
12
|
}
|
|
12
|
-
export interface QuestionFormProps {
|
|
13
|
+
export interface QuestionFormProps extends UserTextRedactionOptions {
|
|
13
14
|
form: QuestionFormType;
|
|
14
15
|
/** Whether the user can still submit answers — the host disables this once the turn is no longer the most recent one. */
|
|
15
16
|
interactive: boolean;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CREDENTIAL_CARD_GUIDANCE, SECRET_REDACTED_NOTICE } from '../../core/user-text-redaction.js';
|
|
3
|
+
import { useUserTextGuard } from '../hooks/useUserTextGuard.js';
|
|
2
4
|
/**
|
|
3
5
|
* @module QuestionForm
|
|
4
6
|
*
|
|
@@ -13,15 +15,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
15
|
* string wrapped in `useT()`.
|
|
14
16
|
*/
|
|
15
17
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
|
16
|
-
import { formatFormAnswers, formOptionValueForLabel } from '../../core/index.js';
|
|
18
|
+
import { formatFormAnswers, formOptionValueForLabel, redactQuestionAnswers, isSecretQuestion, withoutSecretQuestionDefaults, questionTextInputProps, questionTextareaProps } from '../../core/index.js';
|
|
17
19
|
import { useT } from '../hooks/context.js';
|
|
18
|
-
export const QuestionForm = forwardRef(function QuestionForm({ form, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit }, ref) {
|
|
20
|
+
export const QuestionForm = forwardRef(function QuestionForm({ form: incomingForm, interactive, submittedAnswers, hideInternalSubmit = false, draftAnswers, onReadyChange, onDraftChange, onAnswerChange, onSubmit, redactUserText, onSecretRedacted }, ref) {
|
|
19
21
|
const t = useT();
|
|
22
|
+
const form = useMemo(() => withoutSecretQuestionDefaults({ form: incomingForm }), [incomingForm]);
|
|
23
|
+
const guard = useUserTextGuard({ ...(redactUserText ? { redactUserText } : {}), ...(onSecretRedacted ? { onSecretRedacted } : {}) });
|
|
20
24
|
const initial = useMemo(() => buildInitialState(form, submittedAnswers, draftAnswers), [form, submittedAnswers, draftAnswers]);
|
|
21
25
|
const [answers, setAnswers] = useState(initial);
|
|
22
26
|
const [fileAnswers, setFileAnswers] = useState({});
|
|
23
27
|
const locked = !interactive || !onSubmit || submittedAnswers !== undefined;
|
|
24
|
-
const currentAnswers = submittedAnswers
|
|
28
|
+
const currentAnswers = submittedAnswers ? redactQuestionAnswers({ form, answers: submittedAnswers }).answers : answers;
|
|
25
29
|
useEffect(() => {
|
|
26
30
|
setFileAnswers({});
|
|
27
31
|
}, [form.id]);
|
|
@@ -51,8 +55,8 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
|
|
|
51
55
|
return;
|
|
52
56
|
const next = { ...answers, [id]: value };
|
|
53
57
|
setAnswers(next);
|
|
54
|
-
onDraftChange?.(draftSafeAnswers(form, next));
|
|
55
|
-
onAnswerChange?.(id, value);
|
|
58
|
+
onDraftChange?.(redactQuestionAnswers({ form, answers: draftSafeAnswers(form, next) }).answers);
|
|
59
|
+
onAnswerChange?.(id, redactQuestionAnswers({ form, answers: { [id]: value } }).answers[id]);
|
|
56
60
|
}
|
|
57
61
|
function toggleCheckbox(id, option, maxSelections) {
|
|
58
62
|
if (locked)
|
|
@@ -64,7 +68,7 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
|
|
|
64
68
|
const next = has ? current.filter((v) => v !== option) : [...current, option];
|
|
65
69
|
const nextAnswers = { ...answers, [id]: next };
|
|
66
70
|
setAnswers(nextAnswers);
|
|
67
|
-
onDraftChange?.(draftSafeAnswers(form, nextAnswers));
|
|
71
|
+
onDraftChange?.(redactQuestionAnswers({ form, answers: draftSafeAnswers(form, nextAnswers) }).answers);
|
|
68
72
|
onAnswerChange?.(id, next);
|
|
69
73
|
}
|
|
70
74
|
function updateCheckboxCustom(q, raw) {
|
|
@@ -79,11 +83,13 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
|
|
|
79
83
|
return;
|
|
80
84
|
if (!ready)
|
|
81
85
|
return;
|
|
86
|
+
const safe = redactQuestionAnswers({ form, answers }, { ...(redactUserText ? { redactUserText } : {}), onSecretRedacted: guard.notifyRedaction });
|
|
87
|
+
setAnswers(safe.answers);
|
|
82
88
|
const files = collectFileSubmissions(form, fileAnswers);
|
|
83
89
|
if (files.length > 0)
|
|
84
|
-
onSubmit(formatFormAnswers({ form: form, answers: answers }), answers, files);
|
|
90
|
+
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers, files);
|
|
85
91
|
else
|
|
86
|
-
onSubmit(formatFormAnswers({ form: form, answers: answers }), answers);
|
|
92
|
+
onSubmit(formatFormAnswers({ form: form, answers: safe.answers }), safe.answers);
|
|
87
93
|
}
|
|
88
94
|
function handleSkipAll() {
|
|
89
95
|
if (locked || !onSubmit)
|
|
@@ -113,18 +119,18 @@ export const QuestionForm = forwardRef(function QuestionForm({ form, interactive
|
|
|
113
119
|
}, [onReadyChange, locked, ready]);
|
|
114
120
|
return (_jsxs("div", { className: `question-form${locked ? ' question-form-locked' : ''}`, "data-form-id": form.id, children: [_jsxs("div", { className: "question-form-head", children: [_jsx("span", { className: "question-form-icon", "aria-hidden": true, children: "?" }), _jsxs("div", { className: "question-form-titles", children: [_jsx("div", { className: "question-form-title", children: form.title }), form.description ? _jsx("div", { className: "question-form-desc", children: form.description }) : null] }), locked ? _jsx("span", { className: "question-form-pill", children: t('Answered') }) : null] }), _jsx("div", { className: "question-form-body", children: form.questions.map((q) => {
|
|
115
121
|
const value = currentAnswers[q.id];
|
|
116
|
-
return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, q.type === 'radio' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${form.id}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) })) : null, q.type === 'radio' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'checkbox' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
|
|
122
|
+
return (_jsxs("div", { className: "qf-field", children: [_jsxs("label", { className: "qf-label", children: [_jsx("span", { children: q.label }), q.required ? (_jsx("span", { className: "qf-required", "aria-label": t('required'), children: "*" })) : null] }), q.help ? _jsx("div", { className: "qf-help", children: q.help }) : null, isSecretQuestion({ question: q }) ? _jsx("div", { className: "qf-help", children: t(CREDENTIAL_CARD_GUIDANCE) }) : null, q.type === 'radio' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => (_jsxs("label", { className: `qf-chip${value === opt.value ? ' qf-chip-on' : ''}`, title: opt.description, children: [_jsx("input", { type: "radio", name: `${form.id}-${q.id}`, value: opt.value, checked: value === opt.value, disabled: locked, "aria-label": opt.label, onChange: () => update(q.id, opt.value) }), _jsx(OptionCopy, { option: opt })] }, opt.value))) })) : null, q.type === 'radio' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'checkbox' && q.options ? (_jsx("div", { className: "qf-options", children: q.options.map((opt) => {
|
|
117
123
|
const arr = Array.isArray(value) ? value : [];
|
|
118
124
|
const on = arr.includes(opt.value);
|
|
119
125
|
const maxed = q.maxSelections !== undefined && !on && arr.length >= q.maxSelections;
|
|
120
126
|
return (_jsxs("label", { title: opt.description, className: `qf-chip${on ? ' qf-chip-on' : ''}${maxed ? ' qf-chip-disabled' : ''}`, children: [_jsx("input", { type: "checkbox", value: opt.value, checked: on, disabled: locked || maxed, "aria-label": opt.label, onChange: () => toggleCheckbox(q.id, opt.value, q.maxSelections) }), _jsx(OptionCopy, { option: opt })] }, opt.value));
|
|
121
|
-
}) })) : null, q.type === 'checkbox' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customCheckboxValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => updateCheckboxCustom(q, next) })) : null, q.type === 'select' && q.options ? (_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] })) : null, q.type === 'select' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'text' ? _jsx("input", {
|
|
127
|
+
}) })) : null, q.type === 'checkbox' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customCheckboxValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => updateCheckboxCustom(q, next) })) : null, q.type === 'select' && q.options ? (_jsxs("select", { className: "qf-select", value: typeof value === 'string' && questionValueIsKnown(q, value) ? value : '', disabled: locked, onChange: (e) => update(q.id, e.target.value), children: [_jsx("option", { value: "", disabled: true, children: q.placeholder ?? t('Choose one') }), q.options.map((opt) => (_jsx("option", { value: opt.value, title: opt.description, children: opt.label }, opt.value)))] })) : null, q.type === 'select' && q.options && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null, q.type === 'text' ? _jsx("input", { ...questionTextInputProps({ question: q }), className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'number' ? _jsx("input", { type: "number", className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'range' ? (_jsxs("div", { className: "qf-range-wrap", children: [_jsx("input", { type: "range", className: "qf-range", value: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0), min: q.min, max: q.max, step: q.step, disabled: locked, onChange: (e) => update(q.id, e.target.value) }), _jsx("output", { className: "qf-range-value", children: typeof value === 'string' && value.trim() ? value : String(q.min ?? 0) })] })) : null, q.type === 'date' || q.type === 'time' || q.type === 'datetime-local' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'color' ? _jsx("input", { type: "color", className: "qf-color", value: normalizeColorInputValue(value), disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'url' || q.type === 'email' || q.type === 'tel' ? _jsx("input", { type: q.type, className: "qf-input", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'file' ? (_jsxs("div", { className: "qf-file-wrap", children: [_jsx("input", { type: "file", className: "qf-file", multiple: q.multiple, accept: q.accept, disabled: locked, onChange: (e) => {
|
|
122
128
|
const files = Array.from(e.target.files ?? []);
|
|
123
129
|
const names = files.map((file) => file.name);
|
|
124
130
|
setFileAnswers((current) => ({ ...current, [q.id]: files }));
|
|
125
131
|
update(q.id, q.multiple ? names : (names[0] ?? ''));
|
|
126
|
-
} }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })) : null, q.type === 'switch' ? (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })) : null, q.type === 'textarea' ? _jsx("textarea", { className: "qf-textarea", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 ? (_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: form.id, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) })) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null] }, q.id));
|
|
127
|
-
}) }), hideInternalSubmit ? null : (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: submittedAnswers ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: handleSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: form.submitLabel ?? t('Continue') })) : null] }))] }));
|
|
132
|
+
} }), fileValueLabel(value) ? _jsx("div", { className: "qf-file-summary", children: fileValueLabel(value) }) : null] })) : null, q.type === 'switch' ? (_jsxs("label", { className: "qf-switch", children: [_jsx("input", { type: "checkbox", role: "switch", checked: value === 'true', disabled: locked, onChange: (e) => update(q.id, e.target.checked ? 'true' : 'false') }), _jsx("span", { "aria-hidden": true })] })) : null, q.type === 'textarea' ? _jsx("textarea", { ...questionTextareaProps({ question: q }), className: "qf-textarea", value: typeof value === 'string' ? value : '', placeholder: q.placeholder, disabled: locked, rows: 3, onChange: (e) => update(q.id, e.target.value) }) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 ? (_jsx("div", { className: "qf-direction-cards", children: q.cards.map((card) => (_jsx(DirectionCardView, { card: card, formId: form.id, questionId: q.id, selected: value === card.id || value === card.label, disabled: locked, onSelect: () => update(q.id, card.id) }, card.id))) })) : null, q.type === 'direction-cards' && q.cards && q.cards.length > 0 && shouldRenderCustomChoice(q) ? (_jsx(CustomChoiceInput, { label: q.customLabel ?? t('Something else'), value: customSingleValue(q, value), placeholder: q.customPlaceholder ?? t('Type your own answer'), disabled: locked, onChange: (next) => update(q.id, next) })) : null] }, q.id));
|
|
133
|
+
}) }), guard.secretRedacted ? _jsx("div", { role: "status", children: t(SECRET_REDACTED_NOTICE) }) : null, hideInternalSubmit ? null : (_jsxs("div", { className: "question-form-foot", children: [locked ? _jsx("span", { className: "qf-locked-note", children: submittedAnswers ? t('You answered this') : t('A newer message answered this') }) : _jsx("span", { className: "qf-hint", children: t('Answer above, then continue') }), !locked ? (_jsx("button", { type: "button", className: "primary", onClick: handleSubmit, disabled: !ready, title: ready ? t('Submit your answers') : t('Answer the required questions first'), children: form.submitLabel ?? t('Continue') })) : null] }))] }));
|
|
128
134
|
});
|
|
129
135
|
function OptionCopy({ option }) {
|
|
130
136
|
return (_jsxs("span", { className: "qf-chip-copy", children: [_jsx("span", { children: option.label }), option.description ? _jsx("span", { className: "qf-chip-desc", children: option.description }) : null] }));
|