@agent-native/core 0.153.10 → 0.154.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +20 -30
- package/corpus/templates/analytics/actions/search-dashboard-references.ts +58 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/lib/utils.ts +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +1 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +366 -3
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +3 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -2
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +1 -1
- package/corpus/templates/calendar/app/lib/utils.ts +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +32 -5
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +15 -15
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/utils.ts +5 -6
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +43 -8
- package/corpus/templates/clips/desktop/src/app.tsx +26 -12
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +23 -9
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +261 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +21 -3
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +350 -0
- package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
- package/corpus/templates/content/app/i18n-data.ts +10 -10
- package/corpus/templates/content/scripts/check-native-deps.mjs +181 -15
- package/corpus/templates/content/shared/content-landing.ts +5 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +79 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +14 -2
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +2 -2
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +35 -11
- package/corpus/templates/design/actions/get-design-system.ts +103 -3
- package/corpus/templates/design/actions/import-design-tokens.ts +37 -8
- package/corpus/templates/design/actions/import-github.ts +32 -138
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +49 -16
- package/corpus/templates/design/actions/present-design-variants.ts +93 -4
- package/corpus/templates/design/actions/run-design-audit.ts +229 -0
- package/corpus/templates/design/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/design/app/components/design/code-workbench/commands.ts +2 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +17 -4
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +5 -1
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +5 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +81 -5
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/i18n-data.ts +32 -1
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +10 -10
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +83 -11
- package/corpus/templates/design/app/pages/DesignSystems.tsx +14 -0
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +22 -0
- package/corpus/templates/design/changelog/2026-08-13-a-screen-that-would-render-behind-an-invisible-alpine-overla.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-attach-a-ui-screenshot-and-the-design-is-reproduced-not-rein.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-linking-a-design-system-means-you-never-get-generic-directio.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-reports-when-a-screen-ignores-its-linke.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-system-preview-now-shows-how-many-named-tokens-we.md +6 -0
- package/corpus/templates/design/changelog/2026-08-13-your-design-system-prompt-and-reference-screenshots-now-reac.md +6 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +18 -2
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +171 -0
- package/corpus/templates/mail/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +10 -6
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +41 -3
- package/corpus/templates/slides/actions/patch-deck.ts +12 -3
- package/corpus/templates/slides/actions/sync-design-system-with-builder.ts +64 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +193 -20
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +64 -34
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +34 -41
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +119 -91
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +16 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +158 -31
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +30 -14
- package/corpus/templates/slides/app/components/editor/SpeakerNotesPanel.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +4 -1
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +14 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +24 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
- package/corpus/templates/slides/app/global.css +24 -0
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +20 -17
- package/corpus/templates/slides/app/i18n/en-US.ts +19 -10
- package/corpus/templates/slides/app/lib/deck-filter.ts +47 -0
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +15 -4
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +637 -1
- package/corpus/templates/slides/app/lib/image-download.ts +84 -0
- package/corpus/templates/slides/app/lib/utils.ts +1 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
- package/corpus/templates/slides/app/pages/Index.tsx +25 -9
- package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +14 -2
- package/dist/a2a/audience.d.ts +2 -0
- package/dist/a2a/audience.js +23 -0
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.js +32 -14
- package/dist/a2a/server.d.ts +9 -4
- package/dist/a2a/server.js +52 -25
- package/dist/a2a/types.d.ts +4 -4
- package/dist/agent/production-agent.js +1 -1
- package/dist/brand-kit/types.d.ts +10 -2
- package/dist/cli/code.js +1 -1
- package/dist/cli/index.js +30 -1
- package/dist/cli/native-dependencies.d.ts +24 -0
- package/dist/cli/native-dependencies.js +264 -0
- package/dist/cli/plan-local.js +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
- package/dist/cli/skills-content/design-visual-edit-skill.js +2 -2
- package/dist/cli/visualize-repo.js +1 -1
- package/dist/client/FeedbackButton.js +12 -12
- package/dist/client/agent-chat-adapter.js +3 -4
- package/dist/client/assistant-ui-recovery.d.ts +1 -1
- package/dist/client/assistant-ui-recovery.js +4 -0
- package/dist/client/chat/message-components.d.ts +11 -3
- package/dist/client/chat/message-components.js +31 -5
- package/dist/client/chat/tool-call-display.d.ts +2 -1
- package/dist/client/chat/tool-call-display.js +33 -15
- package/dist/client/db-admin/SqlEditor.js +1 -1
- package/dist/client/dev-overlay/DevOverlay.js +2 -2
- package/dist/client/extensions/portable-extension.js +4 -0
- package/dist/client/guided-questions.d.ts +8 -0
- package/dist/client/guided-questions.js +12 -3
- package/dist/client/sharing/ShareButton.js +16 -24
- package/dist/client/sse-event-processor.js +35 -15
- package/dist/client/tool-display.d.ts +9 -0
- package/dist/client/tool-display.js +39 -0
- package/dist/client/vite-dev-recovery-script.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/html-shell.js +5 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
- package/dist/server/agent-chat/action-filters-a2a.js +59 -23
- package/dist/server/agent-chat/prompt-resources.js +17 -16
- package/dist/server/agent-chat-plugin.d.ts +9 -0
- package/dist/server/agent-chat-plugin.js +29 -7
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +33 -1
- package/dist/server/builder-design-systems.js +293 -11
- package/dist/server/design-token-utils.d.ts +11 -0
- package/dist/server/design-token-utils.js +68 -16
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative browser-side mic cleanup for Clips recorders.
|
|
3
|
+
*
|
|
4
|
+
* The goal is not to "fix" bad audio with a heavy denoiser. Instead we trim
|
|
5
|
+
* obvious AC rumble, notch the mains hum that sits under cheap mics, and only
|
|
6
|
+
* apply a gentle downward expander when the signal is already quiet.
|
|
7
|
+
*
|
|
8
|
+
* Safe failure rule: if the browser does not support the needed Web Audio
|
|
9
|
+
* pieces, or if graph setup fails for any reason, callers get the original
|
|
10
|
+
* stream back with a no-op cleanup handle rather than a thrown error.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MIC_AUDIO_HIGH_PASS_HZ = 70;
|
|
14
|
+
export const MIC_AUDIO_HUM_NOTCH_FREQUENCIES = [50, 60, 100, 120] as const;
|
|
15
|
+
export const MIC_AUDIO_HUM_NOTCH_Q = 35;
|
|
16
|
+
export const MIC_AUDIO_EXPANDER_INTERVAL_MS = 40;
|
|
17
|
+
export const MIC_AUDIO_EXPANDER_FLOOR_GAIN = 0.24;
|
|
18
|
+
export const MIC_AUDIO_EXPANDER_OPEN_DB = -42;
|
|
19
|
+
export const MIC_AUDIO_EXPANDER_CLOSE_DB = -58;
|
|
20
|
+
export const MIC_AUDIO_EXPANDER_SMOOTHING_SECONDS = 0.09;
|
|
21
|
+
|
|
22
|
+
export type MicAudioCleanupPreset = "balanced" | "strong";
|
|
23
|
+
|
|
24
|
+
export interface MicAudioCleanupOptions {
|
|
25
|
+
audioContext?: AudioContext | null;
|
|
26
|
+
preset?: MicAudioCleanupPreset;
|
|
27
|
+
intervalMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MicAudioCleanupHandle {
|
|
31
|
+
readonly inputStream: MediaStream;
|
|
32
|
+
readonly stream: MediaStream;
|
|
33
|
+
readonly active: boolean;
|
|
34
|
+
stop(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type AudioContextLike = Pick<
|
|
38
|
+
AudioContext,
|
|
39
|
+
| "state"
|
|
40
|
+
| "currentTime"
|
|
41
|
+
| "resume"
|
|
42
|
+
| "close"
|
|
43
|
+
| "createAnalyser"
|
|
44
|
+
| "createBiquadFilter"
|
|
45
|
+
| "createGain"
|
|
46
|
+
| "createMediaStreamDestination"
|
|
47
|
+
| "createMediaStreamSource"
|
|
48
|
+
>;
|
|
49
|
+
|
|
50
|
+
type AudioNodeLike = {
|
|
51
|
+
connect(destination: unknown): unknown;
|
|
52
|
+
disconnect(): void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type GainNodeLike = AudioNodeLike & {
|
|
56
|
+
gain: AudioParam & {
|
|
57
|
+
setTargetAtTime?: (
|
|
58
|
+
value: number,
|
|
59
|
+
startTime: number,
|
|
60
|
+
timeConstant: number,
|
|
61
|
+
) => void;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type BiquadFilterNodeLike = AudioNodeLike & {
|
|
66
|
+
type: BiquadFilterType;
|
|
67
|
+
frequency: AudioParam;
|
|
68
|
+
Q: AudioParam;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type AnalyserNodeLike = AudioNodeLike & {
|
|
72
|
+
fftSize: number;
|
|
73
|
+
getFloatTimeDomainData: (dataArray: Float32Array<ArrayBuffer>) => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type MediaStreamDestinationNodeLike = AudioNodeLike & {
|
|
77
|
+
stream: MediaStream;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type MediaStreamSourceNodeLike = AudioNodeLike;
|
|
81
|
+
|
|
82
|
+
interface MicAudioCleanupRuntime {
|
|
83
|
+
ctx: AudioContextLike;
|
|
84
|
+
ownsContext: boolean;
|
|
85
|
+
source: MediaStreamSourceNodeLike;
|
|
86
|
+
highPass: BiquadFilterNodeLike;
|
|
87
|
+
notches: BiquadFilterNodeLike[];
|
|
88
|
+
gain: GainNodeLike;
|
|
89
|
+
analyser: AnalyserNodeLike;
|
|
90
|
+
destination: MediaStreamDestinationNodeLike;
|
|
91
|
+
intervalId: ReturnType<typeof setInterval>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getAudioContextCtor(): typeof AudioContext | null {
|
|
95
|
+
const global = globalThis as typeof globalThis & {
|
|
96
|
+
AudioContext?: typeof AudioContext;
|
|
97
|
+
webkitAudioContext?: typeof AudioContext;
|
|
98
|
+
};
|
|
99
|
+
return global.AudioContext ?? global.webkitAudioContext ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizePreset(preset: MicAudioCleanupPreset | undefined) {
|
|
103
|
+
return preset === "strong"
|
|
104
|
+
? {
|
|
105
|
+
floorGain: 0.16,
|
|
106
|
+
openDb: -38,
|
|
107
|
+
closeDb: -56,
|
|
108
|
+
smoothingSeconds: 0.07,
|
|
109
|
+
intervalMs: 32,
|
|
110
|
+
}
|
|
111
|
+
: {
|
|
112
|
+
floorGain: MIC_AUDIO_EXPANDER_FLOOR_GAIN,
|
|
113
|
+
openDb: MIC_AUDIO_EXPANDER_OPEN_DB,
|
|
114
|
+
closeDb: MIC_AUDIO_EXPANDER_CLOSE_DB,
|
|
115
|
+
smoothingSeconds: MIC_AUDIO_EXPANDER_SMOOTHING_SECONDS,
|
|
116
|
+
intervalMs: MIC_AUDIO_EXPANDER_INTERVAL_MS,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasAudioTrack(stream: MediaStream): boolean {
|
|
121
|
+
try {
|
|
122
|
+
return stream.getAudioTracks().length > 0;
|
|
123
|
+
// coercion-ok: an unreadable track is a typed inactive cleanup fallback.
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildTargetGain(
|
|
130
|
+
levelDb: number,
|
|
131
|
+
preset: ReturnType<typeof normalizePreset>,
|
|
132
|
+
) {
|
|
133
|
+
if (!Number.isFinite(levelDb)) return preset.floorGain;
|
|
134
|
+
if (levelDb >= preset.openDb) return 1;
|
|
135
|
+
if (levelDb <= preset.closeDb) return preset.floorGain;
|
|
136
|
+
|
|
137
|
+
const span = preset.openDb - preset.closeDb;
|
|
138
|
+
const t = (levelDb - preset.closeDb) / span;
|
|
139
|
+
// Ease in so the expander stays out of the way until the signal is
|
|
140
|
+
// genuinely quiet, then falls away a little more decisively.
|
|
141
|
+
return preset.floorGain + (1 - preset.floorGain) * t * t;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function measureRmsDb(samples: Float32Array): number {
|
|
145
|
+
let sumSquares = 0;
|
|
146
|
+
for (let i = 0; i < samples.length; i++) {
|
|
147
|
+
const sample = samples[i] ?? 0;
|
|
148
|
+
sumSquares += sample * sample;
|
|
149
|
+
}
|
|
150
|
+
if (sumSquares <= 0) return Number.NEGATIVE_INFINITY;
|
|
151
|
+
const rms = Math.sqrt(sumSquares / samples.length);
|
|
152
|
+
if (rms <= 0) return Number.NEGATIVE_INFINITY;
|
|
153
|
+
return 20 * Math.log10(rms);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function disconnectNode(node: AudioNodeLike | null | undefined): void {
|
|
157
|
+
if (!node) return;
|
|
158
|
+
try {
|
|
159
|
+
node.disconnect();
|
|
160
|
+
// coercion-ok: disconnect is teardown-only and may race an ended track.
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore — cleanup must be best-effort.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stopDestinationStream(stream: MediaStream): void {
|
|
167
|
+
try {
|
|
168
|
+
for (const track of stream.getTracks()) {
|
|
169
|
+
try {
|
|
170
|
+
track.stop();
|
|
171
|
+
// coercion-ok: stopping an already-ended destination track is cleanup-only.
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// coercion-ok: destination teardown is best-effort after capture has ended.
|
|
177
|
+
} catch {
|
|
178
|
+
// ignore
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createInactiveHandle(stream: MediaStream): MicAudioCleanupHandle {
|
|
183
|
+
return {
|
|
184
|
+
inputStream: stream,
|
|
185
|
+
stream,
|
|
186
|
+
active: false,
|
|
187
|
+
stop() {
|
|
188
|
+
// no-op
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function tryBuildMicAudioCleanup(
|
|
194
|
+
stream: MediaStream,
|
|
195
|
+
options: MicAudioCleanupOptions,
|
|
196
|
+
): MicAudioCleanupHandle {
|
|
197
|
+
// The browser path can hand us the actual `AudioContext` instance, while
|
|
198
|
+
// tests can inject a fake object. If neither exists, there is nothing to do.
|
|
199
|
+
const audioContextCtor = getAudioContextCtor();
|
|
200
|
+
const providedContext = options.audioContext ?? null;
|
|
201
|
+
const audioContext: AudioContextLike | null =
|
|
202
|
+
providedContext ?? (audioContextCtor ? new audioContextCtor() : null);
|
|
203
|
+
const ownsContext = !providedContext;
|
|
204
|
+
|
|
205
|
+
if (!audioContext || !hasAudioTrack(stream)) {
|
|
206
|
+
return createInactiveHandle(stream);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const preset = normalizePreset(options.preset);
|
|
210
|
+
const intervalMs = Math.max(
|
|
211
|
+
16,
|
|
212
|
+
Math.round(options.intervalMs ?? preset.intervalMs),
|
|
213
|
+
);
|
|
214
|
+
const supportsNodes =
|
|
215
|
+
typeof audioContext.createAnalyser === "function" &&
|
|
216
|
+
typeof audioContext.createBiquadFilter === "function" &&
|
|
217
|
+
typeof audioContext.createGain === "function" &&
|
|
218
|
+
typeof audioContext.createMediaStreamDestination === "function" &&
|
|
219
|
+
typeof audioContext.createMediaStreamSource === "function";
|
|
220
|
+
if (!supportsNodes) {
|
|
221
|
+
return createInactiveHandle(stream);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let runtime: MicAudioCleanupRuntime | null = null;
|
|
225
|
+
try {
|
|
226
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
227
|
+
const highPass = audioContext.createBiquadFilter();
|
|
228
|
+
highPass.type = "highpass";
|
|
229
|
+
highPass.frequency.value = MIC_AUDIO_HIGH_PASS_HZ;
|
|
230
|
+
highPass.Q.value = 0.707;
|
|
231
|
+
|
|
232
|
+
const notches = MIC_AUDIO_HUM_NOTCH_FREQUENCIES.map((frequency) => {
|
|
233
|
+
const filter = audioContext.createBiquadFilter();
|
|
234
|
+
filter.type = "notch";
|
|
235
|
+
filter.frequency.value = frequency;
|
|
236
|
+
filter.Q.value = MIC_AUDIO_HUM_NOTCH_Q;
|
|
237
|
+
return filter;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const gain = audioContext.createGain();
|
|
241
|
+
gain.gain.value = 1;
|
|
242
|
+
const analyser = audioContext.createAnalyser();
|
|
243
|
+
analyser.fftSize = 1024;
|
|
244
|
+
const destination = audioContext.createMediaStreamDestination();
|
|
245
|
+
|
|
246
|
+
source.connect(highPass);
|
|
247
|
+
let node: AudioNodeLike = highPass;
|
|
248
|
+
for (const notch of notches) {
|
|
249
|
+
node.connect(notch);
|
|
250
|
+
node = notch;
|
|
251
|
+
}
|
|
252
|
+
node.connect(gain);
|
|
253
|
+
node.connect(analyser);
|
|
254
|
+
gain.connect(destination);
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
audioContext.state === "suspended" &&
|
|
258
|
+
typeof audioContext.resume === "function"
|
|
259
|
+
) {
|
|
260
|
+
void audioContext.resume().catch(() => {});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const samples = new Float32Array(analyser.fftSize);
|
|
264
|
+
const tick = () => {
|
|
265
|
+
if (!runtime) return;
|
|
266
|
+
try {
|
|
267
|
+
runtime.analyser.getFloatTimeDomainData(samples);
|
|
268
|
+
const nextGain = buildTargetGain(measureRmsDb(samples), preset);
|
|
269
|
+
const now = runtime.ctx.currentTime;
|
|
270
|
+
if (typeof runtime.gain.gain.setTargetAtTime === "function") {
|
|
271
|
+
runtime.gain.gain.setTargetAtTime(
|
|
272
|
+
nextGain,
|
|
273
|
+
now,
|
|
274
|
+
preset.smoothingSeconds,
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
runtime.gain.gain.value = nextGain;
|
|
278
|
+
}
|
|
279
|
+
// coercion-ok: a transient analyser/meter failure must not stop recording.
|
|
280
|
+
} catch {
|
|
281
|
+
// Keep the recording alive even if the meter path glitches.
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const intervalId = globalThis.setInterval(tick, intervalMs);
|
|
286
|
+
runtime = {
|
|
287
|
+
ctx: audioContext,
|
|
288
|
+
ownsContext,
|
|
289
|
+
source,
|
|
290
|
+
highPass,
|
|
291
|
+
notches,
|
|
292
|
+
gain,
|
|
293
|
+
analyser,
|
|
294
|
+
destination,
|
|
295
|
+
intervalId,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
inputStream: stream,
|
|
300
|
+
stream: destination.stream,
|
|
301
|
+
active: true,
|
|
302
|
+
stop() {
|
|
303
|
+
if (!runtime) return;
|
|
304
|
+
const current = runtime;
|
|
305
|
+
runtime = null;
|
|
306
|
+
try {
|
|
307
|
+
clearInterval(current.intervalId);
|
|
308
|
+
// coercion-ok: timer cleanup is best-effort after the interval ends.
|
|
309
|
+
} catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
disconnectNode(current.source);
|
|
313
|
+
disconnectNode(current.highPass);
|
|
314
|
+
for (const notch of current.notches) disconnectNode(notch);
|
|
315
|
+
disconnectNode(current.gain);
|
|
316
|
+
disconnectNode(current.analyser);
|
|
317
|
+
stopDestinationStream(current.destination.stream);
|
|
318
|
+
if (current.ownsContext && typeof current.ctx.close === "function") {
|
|
319
|
+
void current.ctx.close().catch(() => {});
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
if (runtime) {
|
|
325
|
+
try {
|
|
326
|
+
clearInterval(runtime.intervalId);
|
|
327
|
+
} catch {
|
|
328
|
+
// ignore
|
|
329
|
+
}
|
|
330
|
+
disconnectNode(runtime.source);
|
|
331
|
+
disconnectNode(runtime.highPass);
|
|
332
|
+
for (const notch of runtime.notches) disconnectNode(notch);
|
|
333
|
+
disconnectNode(runtime.gain);
|
|
334
|
+
disconnectNode(runtime.analyser);
|
|
335
|
+
stopDestinationStream(runtime.destination.stream);
|
|
336
|
+
}
|
|
337
|
+
return createInactiveHandle(stream);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function createMicAudioCleanup(
|
|
342
|
+
stream: MediaStream,
|
|
343
|
+
options: MicAudioCleanupOptions = {},
|
|
344
|
+
): MicAudioCleanupHandle {
|
|
345
|
+
try {
|
|
346
|
+
return tryBuildMicAudioCleanup(stream, options);
|
|
347
|
+
} catch {
|
|
348
|
+
return createInactiveHandle(stream);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { defineAction } from "@agent-native/core";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
compareAndSetAppState,
|
|
6
|
+
listAppState,
|
|
7
|
+
readAppState,
|
|
8
|
+
} from "@agent-native/core/application-state";
|
|
5
9
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
6
10
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
7
11
|
import { z } from "zod";
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
CONTENT_LAST_LOCATION_STATE_KEY,
|
|
15
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
11
16
|
type ContentLandingResolution,
|
|
12
17
|
type ContentLastLocationState,
|
|
18
|
+
type ContentWelcomePageState,
|
|
13
19
|
} from "../shared/content-landing.js";
|
|
14
20
|
import { resolveContentDocumentAccess } from "./_content-document-access.js";
|
|
15
21
|
import { normalizeContentSpaceEmail } from "./_content-space-access.js";
|
|
@@ -24,12 +30,85 @@ const WELCOME_CONTENT = `Start here with a page that is wholly yours.
|
|
|
24
30
|
- Use the sidebar to find and organize your work.
|
|
25
31
|
- Ask the agent when you want a hand.`;
|
|
26
32
|
|
|
27
|
-
function
|
|
33
|
+
function legacyWelcomeDocumentId(userEmail: string, generation: number) {
|
|
28
34
|
const identity = normalizeContentSpaceEmail(userEmail);
|
|
29
|
-
const
|
|
35
|
+
const input = generation === 0 ? identity : `${identity}:${generation}`;
|
|
36
|
+
const digest = createHash("sha256").update(input).digest("hex");
|
|
30
37
|
return `content_welcome_${digest.slice(0, 32)}`;
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
function randomWelcomeDocumentId() {
|
|
41
|
+
return `content_welcome_${randomUUID().replace(/-/g, "")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function welcomeGeneration(state: Record<string, unknown>): number {
|
|
45
|
+
const candidate = state as ContentWelcomePageState;
|
|
46
|
+
if (
|
|
47
|
+
typeof candidate.generation !== "number" ||
|
|
48
|
+
!Number.isSafeInteger(candidate.generation) ||
|
|
49
|
+
candidate.generation < 0
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("Content welcome page state has an invalid generation");
|
|
52
|
+
}
|
|
53
|
+
return candidate.generation;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isUniqueConstraintError(error: unknown): boolean {
|
|
57
|
+
let current: unknown = error;
|
|
58
|
+
const seen = new Set<unknown>();
|
|
59
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
60
|
+
seen.add(current);
|
|
61
|
+
const candidate = current as {
|
|
62
|
+
code?: unknown;
|
|
63
|
+
message?: unknown;
|
|
64
|
+
cause?: unknown;
|
|
65
|
+
};
|
|
66
|
+
const code = String(candidate.code ?? "");
|
|
67
|
+
const message = String(candidate.message ?? "");
|
|
68
|
+
if (
|
|
69
|
+
code === "23505" ||
|
|
70
|
+
code.includes("SQLITE_CONSTRAINT") ||
|
|
71
|
+
/unique constraint|unique violation|duplicate key/i.test(message)
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
current = candidate.cause;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveWelcomeState(): Promise<{
|
|
81
|
+
generation: number;
|
|
82
|
+
documentId: string | null;
|
|
83
|
+
expectedValue: Record<string, unknown>;
|
|
84
|
+
}> {
|
|
85
|
+
while (true) {
|
|
86
|
+
const entries = await listAppState(CONTENT_WELCOME_PAGE_STATE_KEY);
|
|
87
|
+
const entry = entries.find(
|
|
88
|
+
({ key }) => key === CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
89
|
+
);
|
|
90
|
+
if (entry) {
|
|
91
|
+
if (!entry.value || typeof entry.value !== "object") {
|
|
92
|
+
throw new Error("Content welcome page state must be an object");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
generation: welcomeGeneration(entry.value),
|
|
96
|
+
documentId:
|
|
97
|
+
typeof entry.value.documentId === "string" && entry.value.documentId
|
|
98
|
+
? entry.value.documentId
|
|
99
|
+
: null,
|
|
100
|
+
expectedValue: entry.value,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const initial = { generation: 0 };
|
|
105
|
+
if (
|
|
106
|
+
await compareAndSetAppState(CONTENT_WELCOME_PAGE_STATE_KEY, null, initial)
|
|
107
|
+
) {
|
|
108
|
+
return { generation: 0, documentId: null, expectedValue: initial };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
33
112
|
function savedDocumentId(state: Record<string, unknown> | null): string | null {
|
|
34
113
|
const candidate = state as ContentLastLocationState | null;
|
|
35
114
|
return typeof candidate?.documentId === "string" && candidate.documentId
|
|
@@ -44,22 +123,28 @@ async function resolveUsableDocument(documentId: string) {
|
|
|
44
123
|
return access.resource;
|
|
45
124
|
}
|
|
46
125
|
|
|
47
|
-
async function resolveWelcomeDocument(userEmail: string) {
|
|
48
|
-
const
|
|
49
|
-
const document =
|
|
50
|
-
if (!document)
|
|
126
|
+
async function resolveWelcomeDocument(userEmail: string, documentId: string) {
|
|
127
|
+
const access = await resolveContentDocumentAccess(documentId);
|
|
128
|
+
const document = access?.resource;
|
|
129
|
+
if (!document) {
|
|
130
|
+
return (await isSoftDeletedDatabaseDocument(documentId))
|
|
131
|
+
? { status: "unavailable" as const }
|
|
132
|
+
: { status: "missing" as const };
|
|
133
|
+
}
|
|
134
|
+
if (document.trashedAt || (await isSoftDeletedDatabaseDocument(documentId))) {
|
|
135
|
+
return { status: "unavailable" as const };
|
|
136
|
+
}
|
|
51
137
|
|
|
52
138
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
53
139
|
if (
|
|
54
140
|
normalizeContentSpaceEmail(document.ownerEmail) !== normalizedEmail ||
|
|
55
141
|
document.spaceId !== personalContentSpaceId(normalizedEmail) ||
|
|
56
142
|
document.parentId !== null ||
|
|
57
|
-
document.visibility !== "private"
|
|
58
|
-
document.title !== WELCOME_TITLE
|
|
143
|
+
document.visibility !== "private"
|
|
59
144
|
) {
|
|
60
|
-
return
|
|
145
|
+
return { status: "unavailable" as const };
|
|
61
146
|
}
|
|
62
|
-
return documentId;
|
|
147
|
+
return { status: "usable" as const, documentId };
|
|
63
148
|
}
|
|
64
149
|
|
|
65
150
|
async function resolveWelcome(userEmail: string): Promise<{
|
|
@@ -69,27 +154,72 @@ async function resolveWelcome(userEmail: string): Promise<{
|
|
|
69
154
|
"welcome-created" | "welcome-reused"
|
|
70
155
|
>;
|
|
71
156
|
}> {
|
|
72
|
-
const existing = await resolveWelcomeDocument(userEmail);
|
|
73
|
-
if (existing) return { documentId: existing, resolution: "welcome-reused" };
|
|
74
|
-
|
|
75
|
-
const documentId = welcomeDocumentId(userEmail);
|
|
76
157
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
while (true) {
|
|
159
|
+
const state = await resolveWelcomeState();
|
|
160
|
+
const documentId =
|
|
161
|
+
state.documentId ??
|
|
162
|
+
legacyWelcomeDocumentId(normalizedEmail, state.generation);
|
|
163
|
+
const existing = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
164
|
+
if (existing.status === "usable") {
|
|
165
|
+
if (!state.documentId) {
|
|
166
|
+
await compareAndSetAppState(
|
|
167
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
168
|
+
state.expectedValue,
|
|
169
|
+
{ ...state.expectedValue, documentId },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
documentId: existing.documentId,
|
|
174
|
+
resolution: "welcome-reused",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!state.documentId || existing.status === "unavailable") {
|
|
179
|
+
await compareAndSetAppState(
|
|
180
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
181
|
+
state.expectedValue,
|
|
182
|
+
{
|
|
183
|
+
...state.expectedValue,
|
|
184
|
+
generation: state.generation + 1,
|
|
185
|
+
documentId: randomWelcomeDocumentId(),
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await runWithRequestContext({ userEmail: normalizedEmail }, () =>
|
|
193
|
+
createDocumentAction.run({
|
|
194
|
+
id: documentId,
|
|
195
|
+
title: WELCOME_TITLE,
|
|
196
|
+
content: WELCOME_CONTENT,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
return { documentId, resolution: "welcome-created" };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const raced = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
202
|
+
if (raced.status === "usable") {
|
|
203
|
+
return {
|
|
204
|
+
documentId: raced.documentId,
|
|
205
|
+
resolution: "welcome-reused",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (raced.status === "unavailable") continue;
|
|
209
|
+
if (isUniqueConstraintError(error)) {
|
|
210
|
+
await compareAndSetAppState(
|
|
211
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
212
|
+
state.expectedValue,
|
|
213
|
+
{
|
|
214
|
+
...state.expectedValue,
|
|
215
|
+
generation: state.generation + 1,
|
|
216
|
+
documentId: randomWelcomeDocumentId(),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
93
223
|
}
|
|
94
224
|
}
|
|
95
225
|
|
|
@@ -2915,7 +2915,7 @@ const editorSlashMessages = {
|
|
|
2915
2915
|
equationPreview: "Preview",
|
|
2916
2916
|
equationPreviewEmpty: "Your equation will appear here.",
|
|
2917
2917
|
equationSubmitHint:
|
|
2918
|
-
"Preview updates as you type. Press Cmd/Ctrl
|
|
2918
|
+
"Preview updates as you type. Press Cmd/Ctrl Enter to insert.",
|
|
2919
2919
|
quote: "Quote",
|
|
2920
2920
|
quoteDescription: "Block quote",
|
|
2921
2921
|
table: "Table",
|
|
@@ -11195,7 +11195,7 @@ const contentExactEnglishTranslations = {
|
|
|
11195
11195
|
equationPlaceholder: String.raw`输入 LaTeX,例如 \frac{a}{b}`,
|
|
11196
11196
|
equationPreview: "预览",
|
|
11197
11197
|
equationPreviewEmpty: "公式将在此处显示。",
|
|
11198
|
-
equationSubmitHint: "输入时会实时更新预览。按 Cmd/Ctrl
|
|
11198
|
+
equationSubmitHint: "输入时会实时更新预览。按 Cmd/Ctrl Enter 插入。",
|
|
11199
11199
|
},
|
|
11200
11200
|
toolbar: {
|
|
11201
11201
|
info: "信息",
|
|
@@ -11242,7 +11242,7 @@ const contentExactEnglishTranslations = {
|
|
|
11242
11242
|
equationPreview: "Vista previa",
|
|
11243
11243
|
equationPreviewEmpty: "Tu ecuación aparecerá aquí.",
|
|
11244
11244
|
equationSubmitHint:
|
|
11245
|
-
"La vista previa se actualiza mientras escribes. Pulsa Cmd/Ctrl
|
|
11245
|
+
"La vista previa se actualiza mientras escribes. Pulsa Cmd/Ctrl Enter para insertar.",
|
|
11246
11246
|
},
|
|
11247
11247
|
toolbar: {
|
|
11248
11248
|
info: "Información",
|
|
@@ -11291,7 +11291,7 @@ const contentExactEnglishTranslations = {
|
|
|
11291
11291
|
equationPreview: "Aperçu",
|
|
11292
11292
|
equationPreviewEmpty: "Votre équation apparaîtra ici.",
|
|
11293
11293
|
equationSubmitHint:
|
|
11294
|
-
"L’aperçu se met à jour pendant la saisie. Appuyez sur Cmd/Ctrl
|
|
11294
|
+
"L’aperçu se met à jour pendant la saisie. Appuyez sur Cmd/Ctrl Entrée pour insérer.",
|
|
11295
11295
|
},
|
|
11296
11296
|
toolbar: {
|
|
11297
11297
|
info: "Informations",
|
|
@@ -11341,7 +11341,7 @@ const contentExactEnglishTranslations = {
|
|
|
11341
11341
|
equationPreview: "Vorschau",
|
|
11342
11342
|
equationPreviewEmpty: "Ihre Gleichung wird hier angezeigt.",
|
|
11343
11343
|
equationSubmitHint:
|
|
11344
|
-
"Die Vorschau wird während der Eingabe aktualisiert. Mit Cmd/Ctrl
|
|
11344
|
+
"Die Vorschau wird während der Eingabe aktualisiert. Mit Cmd/Ctrl Enter einfügen.",
|
|
11345
11345
|
},
|
|
11346
11346
|
toolbar: {
|
|
11347
11347
|
info: "Informationen",
|
|
@@ -11390,7 +11390,7 @@ const contentExactEnglishTranslations = {
|
|
|
11390
11390
|
equationPreview: "プレビュー",
|
|
11391
11391
|
equationPreviewEmpty: "ここに数式が表示されます。",
|
|
11392
11392
|
equationSubmitHint:
|
|
11393
|
-
"入力中にプレビューが更新されます。Cmd/Ctrl
|
|
11393
|
+
"入力中にプレビューが更新されます。Cmd/Ctrl Enter で挿入します。",
|
|
11394
11394
|
},
|
|
11395
11395
|
toolbar: {
|
|
11396
11396
|
info: "情報",
|
|
@@ -11438,7 +11438,7 @@ const contentExactEnglishTranslations = {
|
|
|
11438
11438
|
equationPreview: "미리보기",
|
|
11439
11439
|
equationPreviewEmpty: "여기에 수식이 표시됩니다.",
|
|
11440
11440
|
equationSubmitHint:
|
|
11441
|
-
"입력하는 동안 미리보기가 업데이트됩니다. Cmd/Ctrl
|
|
11441
|
+
"입력하는 동안 미리보기가 업데이트됩니다. Cmd/Ctrl Enter로 삽입하세요.",
|
|
11442
11442
|
},
|
|
11443
11443
|
toolbar: {
|
|
11444
11444
|
info: "정보",
|
|
@@ -11487,7 +11487,7 @@ const contentExactEnglishTranslations = {
|
|
|
11487
11487
|
equationPreview: "Prévia",
|
|
11488
11488
|
equationPreviewEmpty: "Sua equação aparecerá aqui.",
|
|
11489
11489
|
equationSubmitHint:
|
|
11490
|
-
"A prévia é atualizada enquanto você digita. Pressione Cmd/Ctrl
|
|
11490
|
+
"A prévia é atualizada enquanto você digita. Pressione Cmd/Ctrl Enter para inserir.",
|
|
11491
11491
|
},
|
|
11492
11492
|
toolbar: {
|
|
11493
11493
|
info: "Informações",
|
|
@@ -11535,7 +11535,7 @@ const contentExactEnglishTranslations = {
|
|
|
11535
11535
|
equationPreview: "पूर्वावलोकन",
|
|
11536
11536
|
equationPreviewEmpty: "आपका समीकरण यहाँ दिखाई देगा।",
|
|
11537
11537
|
equationSubmitHint:
|
|
11538
|
-
"लिखते समय पूर्वावलोकन अपडेट होता है। सम्मिलित करने के लिए Cmd/Ctrl
|
|
11538
|
+
"लिखते समय पूर्वावलोकन अपडेट होता है। सम्मिलित करने के लिए Cmd/Ctrl Enter दबाएँ।",
|
|
11539
11539
|
},
|
|
11540
11540
|
toolbar: {
|
|
11541
11541
|
info: "जानकारी",
|
|
@@ -11581,7 +11581,7 @@ const contentExactEnglishTranslations = {
|
|
|
11581
11581
|
equationPreview: "معاينة",
|
|
11582
11582
|
equationPreviewEmpty: "ستظهر معادلتك هنا.",
|
|
11583
11583
|
equationSubmitHint:
|
|
11584
|
-
"تتحدث المعاينة أثناء الكتابة. اضغط Cmd/Ctrl
|
|
11584
|
+
"تتحدث المعاينة أثناء الكتابة. اضغط Cmd/Ctrl Enter للإدراج.",
|
|
11585
11585
|
},
|
|
11586
11586
|
toolbar: {
|
|
11587
11587
|
info: "معلومات",
|