@opengeni/react 3.1.0 → 3.3.2-canary.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/dist/accounts.js +87 -11
- package/dist/accounts.js.map +1 -1
- package/dist/{browser-viewer-AEFHUYZK.js → browser-viewer-MKKFRRRV.js} +3 -3
- package/dist/{chunk-YFKJ7VZH.js → chunk-7EAFGICR.js} +45 -11
- package/dist/chunk-7EAFGICR.js.map +1 -0
- package/dist/{chunk-W6NFJXSA.js → chunk-7KBJLTKS.js} +52 -12
- package/dist/chunk-7KBJLTKS.js.map +1 -0
- package/dist/{chunk-YJFGAFBD.js → chunk-E5KISKFG.js} +241 -97
- package/dist/chunk-E5KISKFG.js.map +1 -0
- package/dist/{chunk-MZ2G2XBA.js → chunk-IOLVN74D.js} +31 -4
- package/dist/chunk-IOLVN74D.js.map +1 -0
- package/dist/{chunk-D3UIAJKY.js → chunk-JAQ5K7W6.js} +3 -2
- package/dist/{chunk-D3UIAJKY.js.map → chunk-JAQ5K7W6.js.map} +1 -1
- package/dist/{chunk-LAKLF4PA.js → chunk-VTOREEXF.js} +8 -7
- package/dist/chunk-VTOREEXF.js.map +1 -0
- package/dist/{chunk-X4A66EWZ.js → chunk-X5S426G2.js} +38 -44
- package/dist/chunk-X5S426G2.js.map +1 -0
- package/dist/{chunk-HQVCKCLG.js → chunk-YG4QEQF5.js} +354 -255
- package/dist/chunk-YG4QEQF5.js.map +1 -0
- package/dist/{chunk-XXWH2JLC.js → chunk-YVCHAQZY.js} +2 -2
- package/dist/{chunk-RGALCTWO.js → chunk-ZKNTK7IA.js} +13 -9
- package/dist/{chunk-RGALCTWO.js.map → chunk-ZKNTK7IA.js.map} +1 -1
- package/dist/{chunk-OAM4WAAM.js → chunk-ZPBTI3TE.js} +2 -2
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +9 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-file-attachments.d.ts +2 -0
- package/dist/hooks/use-session-events.d.ts +3 -3
- package/dist/index.js +77 -69
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/machines.js +2 -2
- package/dist/model-policy.d.ts +2 -1
- package/dist/model-policy.js +1 -1
- package/dist/realtime.js +47 -8
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +4 -4
- package/dist/session.js +4 -4
- package/dist/timeline/entrance.d.ts +13 -1
- package/package.json +3 -2
- package/src/accounts.tsx +116 -14
- package/src/components/composer.tsx +15 -5
- package/src/components/fleet-tile.tsx +4 -15
- package/src/components/message-timeline.tsx +237 -101
- package/src/components/model-policy-picker.tsx +59 -13
- package/src/components/tip-follow.ts +52 -0
- package/src/components/tooltip.tsx +38 -5
- package/src/hooks/use-available-models.ts +20 -14
- package/src/hooks/use-composer.ts +49 -3
- package/src/hooks/use-file-attachments.ts +28 -2
- package/src/hooks/use-goal.ts +31 -7
- package/src/hooks/use-last-started-turn-policy.ts +11 -5
- package/src/hooks/use-session-events.ts +186 -61
- package/src/hooks/use-session-lineage.ts +20 -16
- package/src/hooks/use-session.ts +23 -19
- package/src/hooks/use-workspace-sessions.ts +31 -27
- package/src/lib/format.ts +8 -6
- package/src/model-policy.ts +45 -54
- package/src/realtime/realtime-control.tsx +55 -6
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/projection.ts +4 -0
- package/styles/compiled.css +5 -0
- package/dist/chunk-HQVCKCLG.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-MZ2G2XBA.js.map +0 -1
- package/dist/chunk-W6NFJXSA.js.map +0 -1
- package/dist/chunk-X4A66EWZ.js.map +0 -1
- package/dist/chunk-YFKJ7VZH.js.map +0 -1
- package/dist/chunk-YJFGAFBD.js.map +0 -1
- /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-MKKFRRRV.js.map} +0 -0
- /package/dist/{chunk-XXWH2JLC.js.map → chunk-YVCHAQZY.js.map} +0 -0
- /package/dist/{chunk-OAM4WAAM.js.map → chunk-ZPBTI3TE.js.map} +0 -0
package/src/model-policy.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareModelPickerOrder,
|
|
3
|
+
modelPickerBillingClassFor,
|
|
4
|
+
type ModelPickerBillingClass,
|
|
5
|
+
} from "@opengeni/sdk/model-picker-order";
|
|
1
6
|
import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@opengeni/sdk";
|
|
2
7
|
|
|
3
|
-
export type PickerBillingClass =
|
|
4
|
-
| "opengeni_credits"
|
|
5
|
-
| "external"
|
|
6
|
-
| "codex_subscription"
|
|
7
|
-
| "supergrok_subscription"
|
|
8
|
-
| "byok";
|
|
8
|
+
export type PickerBillingClass = ModelPickerBillingClass;
|
|
9
9
|
|
|
10
10
|
export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogModel> = {
|
|
11
11
|
id: string;
|
|
@@ -23,20 +23,12 @@ export type PickerModelRow<TCatalog extends ClientModel = WorkspaceModelCatalogM
|
|
|
23
23
|
|
|
24
24
|
export type LatencyModeId = "standard" | "priority" | "fast";
|
|
25
25
|
|
|
26
|
-
const BILLING_CLASS_ORDER: PickerBillingClass[] = [
|
|
27
|
-
"opengeni_credits",
|
|
28
|
-
"external",
|
|
29
|
-
"codex_subscription",
|
|
30
|
-
"supergrok_subscription",
|
|
31
|
-
"byok",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
26
|
const BILLING_CLASS_LABELS: Record<PickerBillingClass, string> = {
|
|
35
27
|
opengeni_credits: "OpenGeni",
|
|
36
28
|
external: "External",
|
|
37
29
|
codex_subscription: "Codex",
|
|
38
30
|
supergrok_subscription: "SuperGrok",
|
|
39
|
-
byok: "
|
|
31
|
+
byok: "Workspace providers",
|
|
40
32
|
};
|
|
41
33
|
|
|
42
34
|
const AVAILABILITY_REASON_LABELS: Record<string, string> = {
|
|
@@ -50,33 +42,10 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
|
|
|
50
42
|
};
|
|
51
43
|
|
|
52
44
|
export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
53
|
-
if (model.
|
|
54
|
-
return "external";
|
|
55
|
-
}
|
|
56
|
-
if (model.source === "supergrok") {
|
|
57
|
-
return "supergrok_subscription";
|
|
58
|
-
}
|
|
59
|
-
if (model.source === "codex") {
|
|
60
|
-
return "codex_subscription";
|
|
61
|
-
}
|
|
62
|
-
if (model.source === "workspace_gateway") {
|
|
63
|
-
return "byok";
|
|
64
|
-
}
|
|
65
|
-
if (model.credentialSource?.kind === "connected_subscription") {
|
|
66
|
-
return model.credentialSource.provider === "xai"
|
|
67
|
-
? "supergrok_subscription"
|
|
68
|
-
: "codex_subscription";
|
|
69
|
-
}
|
|
70
|
-
if (model.credentialSource?.kind === "workspace_connection") {
|
|
45
|
+
if (model.provider === "workspace-gateway" || model.provider === "workspace-openrouter") {
|
|
71
46
|
return "byok";
|
|
72
47
|
}
|
|
73
|
-
|
|
74
|
-
return "codex_subscription";
|
|
75
|
-
}
|
|
76
|
-
if (model.billing?.upstreamPayer === "workspace") {
|
|
77
|
-
return "byok";
|
|
78
|
-
}
|
|
79
|
-
return "opengeni_credits";
|
|
48
|
+
return modelPickerBillingClassFor(model);
|
|
80
49
|
}
|
|
81
50
|
|
|
82
51
|
export function billingClassLabel(billingClass: PickerBillingClass): string {
|
|
@@ -144,6 +113,23 @@ export function labelReasoningEffort(effort: ReasoningEffort): string {
|
|
|
144
113
|
}
|
|
145
114
|
|
|
146
115
|
export function payerSummaryForModel(model: ClientModel): string {
|
|
116
|
+
if (model.cost === "free") {
|
|
117
|
+
return "Free in this deployment";
|
|
118
|
+
}
|
|
119
|
+
if (model.cost === "credits") {
|
|
120
|
+
return "OpenGeni credits";
|
|
121
|
+
}
|
|
122
|
+
if (model.cost === "subscription") {
|
|
123
|
+
return model.source === "supergrok"
|
|
124
|
+
? "SuperGrok subscription · external billing"
|
|
125
|
+
: "Codex subscription · external billing";
|
|
126
|
+
}
|
|
127
|
+
if (model.cost === "workspace") {
|
|
128
|
+
return workspaceProviderPayerSummary(model);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Older client-config payloads do not carry `cost`; preserve their existing
|
|
132
|
+
// settlement-derived label until every deployment has rolled forward.
|
|
147
133
|
const billing = model.billing;
|
|
148
134
|
if (!billing) {
|
|
149
135
|
return "Route unknown";
|
|
@@ -157,7 +143,7 @@ export function payerSummaryForModel(model: ClientModel): string {
|
|
|
157
143
|
: "Codex subscription · external billing";
|
|
158
144
|
}
|
|
159
145
|
if (billing.upstreamPayer === "workspace") {
|
|
160
|
-
return
|
|
146
|
+
return workspaceProviderPayerSummary(model);
|
|
161
147
|
}
|
|
162
148
|
return "External provider · no OpenGeni credits";
|
|
163
149
|
}
|
|
@@ -175,7 +161,13 @@ export function advancedSourceSummary(model: ClientModel): string | null {
|
|
|
175
161
|
: "Connected Codex subscription";
|
|
176
162
|
}
|
|
177
163
|
if (source.kind === "workspace_connection") {
|
|
178
|
-
|
|
164
|
+
if (model.provider === "workspace-openrouter") {
|
|
165
|
+
return "Workspace OpenRouter connection";
|
|
166
|
+
}
|
|
167
|
+
if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
|
|
168
|
+
return "Workspace Vercel AI Gateway";
|
|
169
|
+
}
|
|
170
|
+
return "Workspace provider connection";
|
|
179
171
|
}
|
|
180
172
|
if (source.kind === "deployment") {
|
|
181
173
|
return source.mechanism === "azure_ad_bearer"
|
|
@@ -185,6 +177,16 @@ export function advancedSourceSummary(model: ClientModel): string | null {
|
|
|
185
177
|
return null;
|
|
186
178
|
}
|
|
187
179
|
|
|
180
|
+
function workspaceProviderPayerSummary(model: ClientModel): string {
|
|
181
|
+
if (model.provider === "workspace-openrouter") {
|
|
182
|
+
return "Billed to the workspace OpenRouter account";
|
|
183
|
+
}
|
|
184
|
+
if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
|
|
185
|
+
return "Billed to the workspace Vercel account";
|
|
186
|
+
}
|
|
187
|
+
return "Billed to the workspace provider account";
|
|
188
|
+
}
|
|
189
|
+
|
|
188
190
|
export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerModelRow[] {
|
|
189
191
|
return models
|
|
190
192
|
.filter((catalog) => {
|
|
@@ -232,18 +234,7 @@ export function projectClientModelRows(models: ClientModel[]): PickerModelRow<Cl
|
|
|
232
234
|
export function sortPickerRows<TCatalog extends ClientModel>(
|
|
233
235
|
rows: PickerModelRow<TCatalog>[],
|
|
234
236
|
): PickerModelRow<TCatalog>[] {
|
|
235
|
-
return [...rows].sort(
|
|
236
|
-
const classDelta =
|
|
237
|
-
BILLING_CLASS_ORDER.indexOf(left.billingClass) -
|
|
238
|
-
BILLING_CLASS_ORDER.indexOf(right.billingClass);
|
|
239
|
-
if (classDelta !== 0) {
|
|
240
|
-
return classDelta;
|
|
241
|
-
}
|
|
242
|
-
if (left.selectable !== right.selectable) {
|
|
243
|
-
return left.selectable ? -1 : 1;
|
|
244
|
-
}
|
|
245
|
-
return left.label.localeCompare(right.label);
|
|
246
|
-
});
|
|
237
|
+
return [...rows].sort(compareModelPickerOrder);
|
|
247
238
|
}
|
|
248
239
|
|
|
249
240
|
export function findPickerRow<TCatalog extends ClientModel>(
|
|
@@ -105,7 +105,12 @@ const REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES = 64;
|
|
|
105
105
|
type RealtimeControllerClient = EmbeddedRealtimeSessionClientLike & SessionRealtimeClientLike;
|
|
106
106
|
|
|
107
107
|
type RealtimeModelCatalogCacheEntry =
|
|
108
|
-
| {
|
|
108
|
+
| {
|
|
109
|
+
state: "loading";
|
|
110
|
+
promise: Promise<RealtimeModelOption[]>;
|
|
111
|
+
controller: AbortController;
|
|
112
|
+
consumers: number;
|
|
113
|
+
}
|
|
109
114
|
| { state: "ready"; models: RealtimeModelOption[]; expiresAt: number };
|
|
110
115
|
|
|
111
116
|
// Scope advisory availability to the exact client object and workspace. The
|
|
@@ -166,19 +171,26 @@ function pruneRealtimeModelCatalogCache(
|
|
|
166
171
|
function loadRealtimeModelCatalog(
|
|
167
172
|
client: RealtimeControllerClient,
|
|
168
173
|
workspaceId: string,
|
|
174
|
+
signal?: AbortSignal,
|
|
169
175
|
): Promise<RealtimeModelOption[]> | null {
|
|
170
176
|
const load = client.getWorkspaceRealtimeModelCatalog;
|
|
171
177
|
if (!load) return null;
|
|
178
|
+
if (signal?.aborted) {
|
|
179
|
+
return Promise.reject(signal.reason ?? new DOMException("Request aborted", "AbortError"));
|
|
180
|
+
}
|
|
172
181
|
const now = Date.now();
|
|
173
182
|
const cache = realtimeModelCatalogCacheFor(client);
|
|
174
183
|
const entry = cache.get(workspaceId);
|
|
175
|
-
if (entry?.state === "loading"
|
|
184
|
+
if (entry?.state === "loading" && !entry.controller.signal.aborted) {
|
|
185
|
+
return consumeRealtimeModelCatalog(entry, signal);
|
|
186
|
+
}
|
|
176
187
|
if (entry?.state === "ready" && entry.expiresAt > now) return Promise.resolve(entry.models);
|
|
177
188
|
if (entry) cache.delete(workspaceId);
|
|
178
189
|
pruneRealtimeModelCatalogCache(cache, now);
|
|
179
190
|
|
|
191
|
+
const controller = new AbortController();
|
|
180
192
|
const promise = load
|
|
181
|
-
.call(client, workspaceId)
|
|
193
|
+
.call(client, workspaceId, { signal: controller.signal })
|
|
182
194
|
.then((response) => response.models.map(toRealtimeModelOption))
|
|
183
195
|
.then((models) => {
|
|
184
196
|
const current = cache.get(workspaceId);
|
|
@@ -197,8 +209,43 @@ function loadRealtimeModelCatalog(
|
|
|
197
209
|
if (current?.state === "loading" && current.promise === promise) cache.delete(workspaceId);
|
|
198
210
|
throw error;
|
|
199
211
|
});
|
|
200
|
-
|
|
201
|
-
|
|
212
|
+
const loading: RealtimeModelCatalogCacheEntry & { state: "loading" } = {
|
|
213
|
+
state: "loading",
|
|
214
|
+
promise,
|
|
215
|
+
controller,
|
|
216
|
+
consumers: 0,
|
|
217
|
+
};
|
|
218
|
+
cache.set(workspaceId, loading);
|
|
219
|
+
return consumeRealtimeModelCatalog(loading, signal);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function consumeRealtimeModelCatalog(
|
|
223
|
+
entry: RealtimeModelCatalogCacheEntry & { state: "loading" },
|
|
224
|
+
signal?: AbortSignal,
|
|
225
|
+
): Promise<RealtimeModelOption[]> {
|
|
226
|
+
entry.consumers += 1;
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
let released = false;
|
|
229
|
+
const release = (abandoned: boolean) => {
|
|
230
|
+
if (released) return;
|
|
231
|
+
released = true;
|
|
232
|
+
entry.consumers = Math.max(0, entry.consumers - 1);
|
|
233
|
+
if (abandoned && entry.consumers === 0) {
|
|
234
|
+
entry.controller.abort(
|
|
235
|
+
signal?.reason ?? new DOMException("Request abandoned", "AbortError"),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const onAbort = () => {
|
|
240
|
+
release(true);
|
|
241
|
+
reject(signal?.reason ?? new DOMException("Request aborted", "AbortError"));
|
|
242
|
+
};
|
|
243
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
244
|
+
void entry.promise.then(resolve, reject).finally(() => {
|
|
245
|
+
signal?.removeEventListener("abort", onAbort);
|
|
246
|
+
release(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
202
249
|
}
|
|
203
250
|
|
|
204
251
|
export type SessionRealtimeControllerFactory = (
|
|
@@ -550,6 +597,7 @@ export function useRealtimeModelSelection(options: {
|
|
|
550
597
|
|
|
551
598
|
useEffect(() => {
|
|
552
599
|
let disposed = false;
|
|
600
|
+
const requestAbort = new AbortController();
|
|
553
601
|
const applyCatalog = (models: RealtimeModelOption[]) => {
|
|
554
602
|
catalogScopeRef.current = { client, workspaceId, source: "cache" };
|
|
555
603
|
setCatalog(models);
|
|
@@ -570,7 +618,7 @@ export function useRealtimeModelSelection(options: {
|
|
|
570
618
|
}
|
|
571
619
|
return;
|
|
572
620
|
}
|
|
573
|
-
const loading = loadRealtimeModelCatalog(client, workspaceId);
|
|
621
|
+
const loading = loadRealtimeModelCatalog(client, workspaceId, requestAbort.signal);
|
|
574
622
|
if (!loading) return;
|
|
575
623
|
void loading
|
|
576
624
|
.then((models) => {
|
|
@@ -580,6 +628,7 @@ export function useRealtimeModelSelection(options: {
|
|
|
580
628
|
.catch(() => undefined);
|
|
581
629
|
return () => {
|
|
582
630
|
disposed = true;
|
|
631
|
+
requestAbort.abort();
|
|
583
632
|
};
|
|
584
633
|
}, [client, workspaceId]);
|
|
585
634
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, useRef } from "react";
|
|
1
|
+
import { createContext, useContext, useRef, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
/* ----------------------------------------------------------------------------
|
|
4
4
|
Entrance animation gating
|
|
@@ -15,8 +15,33 @@ import { createContext, useContext, useRef } from "react";
|
|
|
15
15
|
-------------------------------------------------------------------------- */
|
|
16
16
|
|
|
17
17
|
const EntranceAnimationContext = createContext(true);
|
|
18
|
+
const EntranceAnimationLiveContext = createContext(true);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Freeze the mount-time gate for this subtree while publishing a separate live
|
|
22
|
+
* gate to activity rails that need later appends to animate after a bulk paint.
|
|
23
|
+
* MessageTimeline mounts one provider per durable group, so existing groups do
|
|
24
|
+
* not receive a context invalidation when a prepend toggles bulk mode and new
|
|
25
|
+
* groups still capture the value from the commit that created them.
|
|
26
|
+
*/
|
|
27
|
+
export function EntranceAnimationProvider({
|
|
28
|
+
value,
|
|
29
|
+
liveValue = value,
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
value: boolean;
|
|
33
|
+
liveValue?: boolean | undefined;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}) {
|
|
36
|
+
const mountedValue = useRef(value).current;
|
|
37
|
+
return (
|
|
38
|
+
<EntranceAnimationContext.Provider value={mountedValue}>
|
|
39
|
+
<EntranceAnimationLiveContext.Provider value={liveValue}>
|
|
40
|
+
{children}
|
|
41
|
+
</EntranceAnimationLiveContext.Provider>
|
|
42
|
+
</EntranceAnimationContext.Provider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
22
47
|
* Live entrance gate from the nearest provider. Prefer
|
|
@@ -25,7 +50,7 @@ export const EntranceAnimationProvider = EntranceAnimationContext.Provider;
|
|
|
25
50
|
* later appends after a bulk window clears.
|
|
26
51
|
*/
|
|
27
52
|
export function useEntranceAnimationLive(): boolean {
|
|
28
|
-
return useContext(
|
|
53
|
+
return useContext(EntranceAnimationLiveContext);
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
/**
|
|
@@ -2091,6 +2091,10 @@ function findOpenCall(
|
|
|
2091
2091
|
if (byId) {
|
|
2092
2092
|
return byId as ToolCallItem | WorkerItem;
|
|
2093
2093
|
}
|
|
2094
|
+
// An explicit provider correlation id is authoritative. Falling back to
|
|
2095
|
+
// an unrelated running call corrupts its status/output when compact pages
|
|
2096
|
+
// or interleaved providers deliver an unmatched historical result.
|
|
2097
|
+
return undefined;
|
|
2094
2098
|
}
|
|
2095
2099
|
return reversed.find(
|
|
2096
2100
|
(item): item is ToolCallItem | WorkerItem => isCall(item) && item.status === "running",
|
package/styles/compiled.css
CHANGED
|
@@ -4495,6 +4495,11 @@
|
|
|
4495
4495
|
max-width: 12rem;
|
|
4496
4496
|
}
|
|
4497
4497
|
}
|
|
4498
|
+
:where(.og-root).sm\:max-w-\[32rem\],:where(.og-root) .sm\:max-w-\[32rem\] {
|
|
4499
|
+
@media (width >= 40rem) {
|
|
4500
|
+
max-width: 32rem;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4498
4503
|
:where(.og-root).sm\:max-w-\[82\%\],:where(.og-root) .sm\:max-w-\[82\%\] {
|
|
4499
4504
|
@media (width >= 40rem) {
|
|
4500
4505
|
max-width: 82%;
|