@ifc-lite/viewer 1.17.4 → 1.17.6
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/.turbo/turbo-build.log +16 -16
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +117 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-86rgogji.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-CcPS9MK5.js} +2274 -2227
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-BFUYA08u.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-Bfms9I4A.js} +35160 -33084
- package/dist/assets/index-_bfZsDCC.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-DUyLCMZS.js} +104 -104
- package/dist/assets/{sandbox-DZiNLNMk.js → sandbox-C8575tul.js} +4340 -4322
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-BuZK7OST.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-JsqEGDV8.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +7 -7
- package/src/App.tsx +16 -2
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +195 -91
- package/src/components/viewer/MainToolbar.tsx +4 -3
- package/src/components/viewer/PropertiesPanel.tsx +16 -2
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ViewerLayout.tsx +1 -0
- package/src/components/viewer/Viewport.tsx +14 -2
- package/src/components/viewer/ViewportContainer.tsx +49 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +1 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +484 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/types.ts +14 -2
- package/src/main.tsx +1 -10
- package/src/services/api-keys.ts +73 -0
- package/src/store/constants.ts +20 -2
- package/src/store/index.ts +12 -5
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -18,10 +18,11 @@ import { useWebGPU } from '@/hooks/useWebGPU';
|
|
|
18
18
|
import { openIfcFileDialog } from '@/services/file-dialog';
|
|
19
19
|
import { logToDesktopTerminal } from '@/services/desktop-logger';
|
|
20
20
|
import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRecentFiles, type RecentFileEntry } from '@/lib/recent-files';
|
|
21
|
-
import { isTauri } from '@/
|
|
21
|
+
import { isTauri } from '@/lib/platform';
|
|
22
22
|
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3 } from 'lucide-react';
|
|
23
23
|
import type { MeshData, CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
24
|
-
import {
|
|
24
|
+
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
25
|
+
import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
|
|
25
26
|
|
|
26
27
|
const ZERO_VEC3 = { x: 0, y: 0, z: 0 };
|
|
27
28
|
const DEFAULT_COORDINATE_INFO: CoordinateInfo = {
|
|
@@ -43,6 +44,7 @@ export function ViewportContainer() {
|
|
|
43
44
|
const cesiumEnabled = useViewerStore((s) => s.cesiumEnabled);
|
|
44
45
|
const georefMutations = useViewerStore((s) => s.georefMutations);
|
|
45
46
|
const setCesiumSourceModelId = useViewerStore((s) => s.setCesiumSourceModelId);
|
|
47
|
+
const setCesiumAvailable = useViewerStore((s) => s.setCesiumAvailable);
|
|
46
48
|
// Subscribe to mutationVersion so Cesium reacts to georef edits
|
|
47
49
|
const mutationVersion = useViewerStore((s) => s.mutationVersion);
|
|
48
50
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -176,82 +178,64 @@ export function ViewportContainer() {
|
|
|
176
178
|
const georef = useMemo(() => {
|
|
177
179
|
if (!cesiumEnabled) return null;
|
|
178
180
|
|
|
179
|
-
// Helper: merge original georef with mutations for a model
|
|
180
|
-
function mergeGeoref(
|
|
181
|
-
originalCRS: ProjectedCRS | undefined,
|
|
182
|
-
originalConv: MapConversion | undefined,
|
|
183
|
-
modelId: string,
|
|
184
|
-
): { mapConversion: MapConversion; projectedCRS: ProjectedCRS } | null {
|
|
185
|
-
const muts = georefMutations.get(modelId);
|
|
186
|
-
const mutCRS = muts?.projectedCRS;
|
|
187
|
-
const mutConv = muts?.mapConversion;
|
|
188
|
-
|
|
189
|
-
// Build merged ProjectedCRS — mutation fields override originals
|
|
190
|
-
const hasCRS = originalCRS || mutCRS;
|
|
191
|
-
if (!hasCRS) return null;
|
|
192
|
-
const projectedCRS: ProjectedCRS = {
|
|
193
|
-
id: originalCRS?.id ?? 0,
|
|
194
|
-
name: (mutCRS?.name ?? originalCRS?.name ?? '') as string,
|
|
195
|
-
description: mutCRS?.description ?? originalCRS?.description,
|
|
196
|
-
geodeticDatum: mutCRS?.geodeticDatum ?? originalCRS?.geodeticDatum,
|
|
197
|
-
verticalDatum: mutCRS?.verticalDatum ?? originalCRS?.verticalDatum,
|
|
198
|
-
mapProjection: mutCRS?.mapProjection ?? originalCRS?.mapProjection,
|
|
199
|
-
mapZone: mutCRS?.mapZone ?? originalCRS?.mapZone,
|
|
200
|
-
mapUnit: mutCRS?.mapUnit ?? originalCRS?.mapUnit,
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// Need at least an EPSG name to resolve projection
|
|
204
|
-
if (!projectedCRS.name) return null;
|
|
205
|
-
|
|
206
|
-
// Build merged MapConversion
|
|
207
|
-
const mapConversion: MapConversion = {
|
|
208
|
-
id: originalConv?.id ?? 0,
|
|
209
|
-
sourceCRS: originalConv?.sourceCRS ?? 0,
|
|
210
|
-
targetCRS: originalConv?.targetCRS ?? 0,
|
|
211
|
-
eastings: (mutConv?.eastings ?? originalConv?.eastings ?? 0) as number,
|
|
212
|
-
northings: (mutConv?.northings ?? originalConv?.northings ?? 0) as number,
|
|
213
|
-
orthogonalHeight: (mutConv?.orthogonalHeight ?? originalConv?.orthogonalHeight ?? 0) as number,
|
|
214
|
-
xAxisAbscissa: mutConv?.xAxisAbscissa ?? originalConv?.xAxisAbscissa,
|
|
215
|
-
xAxisOrdinate: mutConv?.xAxisOrdinate ?? originalConv?.xAxisOrdinate,
|
|
216
|
-
scale: mutConv?.scale ?? originalConv?.scale,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
return { mapConversion, projectedCRS };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
181
|
// Check federated models first
|
|
223
182
|
for (const [modelId, model] of storeModels) {
|
|
224
183
|
const ds = model.ifcDataStore;
|
|
225
184
|
if (!ds) continue;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
modelId,
|
|
185
|
+
const effective = getEffectiveGeoreference(
|
|
186
|
+
ds as IfcDataStore,
|
|
187
|
+
model.geometryResult?.coordinateInfo,
|
|
188
|
+
georefMutations.get(modelId),
|
|
231
189
|
);
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
const coordInfo = model.geometryResult?.coordinateInfo;
|
|
235
|
-
return { hasGeoreference: true, ...merged, sourceModelId: modelId, coordinateInfo: coordInfo };
|
|
190
|
+
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
191
|
+
return { ...effective, sourceModelId: modelId };
|
|
236
192
|
}
|
|
237
193
|
}
|
|
238
194
|
|
|
239
195
|
// Fallback to legacy single-model
|
|
240
196
|
if (ifcDataStore) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
'__legacy__',
|
|
197
|
+
const effective = getEffectiveGeoreference(
|
|
198
|
+
ifcDataStore as IfcDataStore,
|
|
199
|
+
mergedGeometryResult?.coordinateInfo,
|
|
200
|
+
georefMutations.get('__legacy__'),
|
|
246
201
|
);
|
|
247
|
-
if (
|
|
248
|
-
return {
|
|
202
|
+
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
203
|
+
return { ...effective, sourceModelId: '__legacy__' };
|
|
249
204
|
}
|
|
250
205
|
}
|
|
251
206
|
|
|
252
207
|
return null;
|
|
253
208
|
}, [cesiumEnabled, storeModels, ifcDataStore, georefMutations, mutationVersion, mergedGeometryResult]);
|
|
254
209
|
|
|
210
|
+
// Determine whether Cesium button should be visible (model has georef or user added it via mutations).
|
|
211
|
+
// Runs independently of cesiumEnabled so the button appears/disappears reactively.
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
function hasGeoref(): boolean {
|
|
214
|
+
// Check federated models
|
|
215
|
+
for (const [modelId, model] of storeModels) {
|
|
216
|
+
const ds = model.ifcDataStore;
|
|
217
|
+
if (!ds) continue;
|
|
218
|
+
const effective = getEffectiveGeoreference(
|
|
219
|
+
ds as IfcDataStore,
|
|
220
|
+
model.geometryResult?.coordinateInfo,
|
|
221
|
+
georefMutations.get(modelId),
|
|
222
|
+
);
|
|
223
|
+
if (effective?.projectedCRS?.name) return true;
|
|
224
|
+
}
|
|
225
|
+
// Fallback to legacy single-model
|
|
226
|
+
if (ifcDataStore) {
|
|
227
|
+
const effective = getEffectiveGeoreference(
|
|
228
|
+
ifcDataStore as IfcDataStore,
|
|
229
|
+
mergedGeometryResult?.coordinateInfo,
|
|
230
|
+
georefMutations.get('__legacy__'),
|
|
231
|
+
);
|
|
232
|
+
if (effective?.projectedCRS?.name) return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
setCesiumAvailable(hasGeoref());
|
|
237
|
+
}, [storeModels, ifcDataStore, georefMutations, mutationVersion, setCesiumAvailable, mergedGeometryResult]);
|
|
238
|
+
|
|
255
239
|
// Sync the active Cesium source model ID so terrain actions are scoped correctly
|
|
256
240
|
useEffect(() => {
|
|
257
241
|
setCesiumSourceModelId(georef?.sourceModelId ?? null);
|
|
@@ -847,13 +831,14 @@ export function ViewportContainer() {
|
|
|
847
831
|
</div>
|
|
848
832
|
)}
|
|
849
833
|
|
|
850
|
-
{/* Cesium 3D world context overlay — rendered behind the WebGPU canvas */}
|
|
851
|
-
{cesiumEnabled && georef && (
|
|
834
|
+
{/* Cesium 3D world context overlay — rendered behind the WebGPU canvas (web only) */}
|
|
835
|
+
{cesiumEnabled && georef && !isTauri() && (
|
|
852
836
|
<CesiumOverlay
|
|
853
837
|
mapConversion={georef.mapConversion}
|
|
854
838
|
projectedCRS={georef.projectedCRS}
|
|
855
839
|
coordinateInfo={georef.coordinateInfo}
|
|
856
840
|
geometryResult={mergedGeometryResult}
|
|
841
|
+
lengthUnitScale={georef.lengthUnitScale}
|
|
857
842
|
/>
|
|
858
843
|
)}
|
|
859
844
|
<Viewport
|
|
@@ -862,7 +847,7 @@ export function ViewportContainer() {
|
|
|
862
847
|
coordinateInfo={mergedGeometryResult?.coordinateInfo}
|
|
863
848
|
computedIsolatedIds={computedIsolatedIds}
|
|
864
849
|
modelIdToIndex={modelIdToIndex}
|
|
865
|
-
cesiumActive={cesiumEnabled && georef !== null}
|
|
850
|
+
cesiumActive={cesiumEnabled && georef !== null && !isTauri()}
|
|
866
851
|
releaseGeometryAfterStream={false}
|
|
867
852
|
onGeometryReleased={releaseGeometryMemory}
|
|
868
853
|
/>
|
|
@@ -21,6 +21,9 @@ import type { CesiumDataSource } from '@/store/slices/cesiumSlice';
|
|
|
21
21
|
import { goHomeFromStore } from '@/store/homeView';
|
|
22
22
|
import { useIfc } from '@/hooks/useIfc';
|
|
23
23
|
import { cn } from '@/lib/utils';
|
|
24
|
+
import { isTauri } from '@/lib/platform';
|
|
25
|
+
|
|
26
|
+
const isDesktop = isTauri();
|
|
24
27
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
25
28
|
import { AxisHelper, type AxisHelperRef } from './AxisHelper';
|
|
26
29
|
|
|
@@ -146,8 +149,8 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
146
149
|
|
|
147
150
|
return (
|
|
148
151
|
<>
|
|
149
|
-
{/* Bottom-right: Cesium settings overlay OR Navigation controls */}
|
|
150
|
-
{cesiumEnabled ? (
|
|
152
|
+
{/* Bottom-right: Cesium settings overlay OR Navigation controls (Cesium is web-only) */}
|
|
153
|
+
{cesiumEnabled && !isDesktop ? (
|
|
151
154
|
<CesiumSettingsOverlay
|
|
152
155
|
dataSource={cesiumDataSource}
|
|
153
156
|
onDataSourceChange={setCesiumDataSource}
|
|
@@ -215,12 +215,12 @@ export function BCFTopicDetail({
|
|
|
215
215
|
<img
|
|
216
216
|
src={vp.snapshot}
|
|
217
217
|
alt="Viewpoint"
|
|
218
|
-
className="w-full
|
|
218
|
+
className="w-full max-h-48 object-contain bg-muted cursor-pointer hover:opacity-90 transition-opacity"
|
|
219
219
|
onClick={() => onActivateViewpoint(vp)}
|
|
220
220
|
/>
|
|
221
221
|
) : (
|
|
222
222
|
<div
|
|
223
|
-
className="w-full aspect-video bg-muted flex items-center justify-center cursor-pointer hover:bg-muted/80 transition-colors"
|
|
223
|
+
className="w-full aspect-video bg-muted flex items-center justify-center cursor-pointer hover:bg-muted/80 transition-colors min-h-[120px]"
|
|
224
224
|
onClick={() => onActivateViewpoint(vp)}
|
|
225
225
|
>
|
|
226
226
|
<Camera className="h-6 w-6 text-muted-foreground" />
|
|
@@ -294,7 +294,7 @@ export function BCFTopicDetail({
|
|
|
294
294
|
<img
|
|
295
295
|
src={associatedViewpoint.snapshot}
|
|
296
296
|
alt="Associated viewpoint"
|
|
297
|
-
className="w-full h-
|
|
297
|
+
className="w-full max-h-24 object-contain bg-muted"
|
|
298
298
|
/>
|
|
299
299
|
</div>
|
|
300
300
|
)}
|
|
@@ -327,7 +327,7 @@ export function BCFTopicDetail({
|
|
|
327
327
|
<img
|
|
328
328
|
src={selectedViewpoint.snapshot}
|
|
329
329
|
alt="Selected viewpoint"
|
|
330
|
-
className="w-
|
|
330
|
+
className="w-12 h-10 object-contain rounded bg-muted"
|
|
331
331
|
/>
|
|
332
332
|
)}
|
|
333
333
|
<div className="flex-1 min-w-0">
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* ModelSelector — dropdown to pick the LLM model.
|
|
7
|
-
* Free models available to everyone
|
|
7
|
+
* Free models available to everyone via server proxy.
|
|
8
|
+
* BYOK models selectable always — greyed out with lock icon when key is missing,
|
|
9
|
+
* fully styled when key is configured. Selecting a locked model triggers the
|
|
10
|
+
* inline key prompt in ChatPanel.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
import { useCallback } from 'react';
|
|
11
|
-
import {
|
|
13
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { Check, Key } from 'lucide-react';
|
|
12
15
|
import {
|
|
13
16
|
Select,
|
|
14
17
|
SelectContent,
|
|
@@ -17,27 +20,43 @@ import {
|
|
|
17
20
|
SelectValue,
|
|
18
21
|
} from '@/components/ui/select';
|
|
19
22
|
import { useViewerStore } from '@/store';
|
|
20
|
-
import { FREE_MODELS,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Whether the user has a pro subscription */
|
|
24
|
-
hasPro?: boolean;
|
|
25
|
-
}
|
|
23
|
+
import { FREE_MODELS, getModelById, getByokModelsForSource } from '@/lib/llm/models';
|
|
24
|
+
import type { LLMModel } from '@/lib/llm/types';
|
|
25
|
+
import { hasAnthropicKey, hasOpenaiKey, subscribeApiKeys } from '@/services/api-keys';
|
|
26
26
|
|
|
27
27
|
function formatContextWindow(tokens: number): string {
|
|
28
28
|
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(0)}M`;
|
|
29
29
|
return `${(tokens / 1_000).toFixed(0)}K`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function CostBadge({ cost }: { cost?: LLMModel['cost'] }) {
|
|
33
|
+
if (!cost) return null;
|
|
34
|
+
const color = cost === '$$$' ? 'text-amber-500' : cost === '$$' ? 'text-blue-500' : 'text-emerald-500';
|
|
35
|
+
return <span className={`text-[10px] font-mono ${color}`}>{cost}</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ModelSelector() {
|
|
33
39
|
const activeModel = useViewerStore((s) => s.chatActiveModel);
|
|
34
40
|
const setActiveModel = useViewerStore((s) => s.setChatActiveModel);
|
|
35
41
|
|
|
42
|
+
const [hasAnthropic, setHasAnthropic] = useState(hasAnthropicKey);
|
|
43
|
+
const [hasOpenai, setHasOpenai] = useState(hasOpenaiKey);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const refresh = () => {
|
|
47
|
+
setHasAnthropic(hasAnthropicKey());
|
|
48
|
+
setHasOpenai(hasOpenaiKey());
|
|
49
|
+
};
|
|
50
|
+
return subscribeApiKeys(refresh);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
36
53
|
const handleChange = useCallback((value: string) => {
|
|
37
54
|
setActiveModel(value);
|
|
38
55
|
}, [setActiveModel]);
|
|
39
56
|
|
|
40
57
|
const current = getModelById(activeModel);
|
|
58
|
+
const anthropicModels = getByokModelsForSource('anthropic');
|
|
59
|
+
const openaiModels = getByokModelsForSource('openai');
|
|
41
60
|
|
|
42
61
|
return (
|
|
43
62
|
<Select value={activeModel} onValueChange={handleChange}>
|
|
@@ -45,57 +64,74 @@ export function ModelSelector({ hasPro = false }: ModelSelectorProps) {
|
|
|
45
64
|
<SelectValue>
|
|
46
65
|
<span className="truncate flex items-center gap-1">
|
|
47
66
|
{current?.name ?? activeModel}
|
|
48
|
-
{current?.cost
|
|
49
|
-
<span className={`text-[10px] font-mono ${
|
|
50
|
-
current.cost === '$$$' ? 'text-amber-500' : current.cost === '$$' ? 'text-blue-500' : 'text-emerald-500'
|
|
51
|
-
}`}>
|
|
52
|
-
{current.cost}
|
|
53
|
-
</span>
|
|
54
|
-
)}
|
|
67
|
+
<CostBadge cost={current?.cost} />
|
|
55
68
|
</span>
|
|
56
69
|
</SelectValue>
|
|
57
70
|
</SelectTrigger>
|
|
58
71
|
<SelectContent>
|
|
59
72
|
{/* Free tier */}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
{FREE_MODELS.length > 0 && (
|
|
74
|
+
<>
|
|
75
|
+
<div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
76
|
+
Free
|
|
77
|
+
</div>
|
|
78
|
+
{FREE_MODELS.map((m) => (
|
|
79
|
+
<SelectItem key={m.id} value={m.id} className="text-xs">
|
|
80
|
+
<span className="flex items-center gap-1.5">
|
|
81
|
+
<span>{m.name}</span>
|
|
82
|
+
<span className="text-muted-foreground text-[10px]">{m.provider}</span>
|
|
83
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
84
|
+
</span>
|
|
85
|
+
</SelectItem>
|
|
86
|
+
))}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Anthropic BYOK */}
|
|
91
|
+
{anthropicModels.length > 0 && (
|
|
92
|
+
<>
|
|
93
|
+
<div className="px-2 py-1 mt-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
|
94
|
+
Anthropic
|
|
95
|
+
{hasAnthropic
|
|
96
|
+
? <Check className="h-2.5 w-2.5 text-emerald-500" />
|
|
97
|
+
: <Key className="h-2.5 w-2.5" />
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
{anthropicModels.map((m) => (
|
|
101
|
+
<SelectItem key={m.id} value={m.id} className={`text-xs ${!hasAnthropic ? 'opacity-50' : ''}`}>
|
|
102
|
+
<span className="flex items-center gap-1.5">
|
|
103
|
+
<span>{m.name}</span>
|
|
104
|
+
<CostBadge cost={m.cost} />
|
|
105
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
106
|
+
{!hasAnthropic && <Key className="h-3 w-3 text-muted-foreground/50" />}
|
|
107
|
+
</span>
|
|
108
|
+
</SelectItem>
|
|
109
|
+
))}
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
72
112
|
|
|
73
|
-
{/*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}`}>
|
|
91
|
-
{m.cost}
|
|
113
|
+
{/* OpenAI BYOK */}
|
|
114
|
+
{openaiModels.length > 0 && (
|
|
115
|
+
<>
|
|
116
|
+
<div className="px-2 py-1 mt-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
|
117
|
+
OpenAI
|
|
118
|
+
{hasOpenai
|
|
119
|
+
? <Check className="h-2.5 w-2.5 text-emerald-500" />
|
|
120
|
+
: <Key className="h-2.5 w-2.5" />
|
|
121
|
+
}
|
|
122
|
+
</div>
|
|
123
|
+
{openaiModels.map((m) => (
|
|
124
|
+
<SelectItem key={m.id} value={m.id} className={`text-xs ${!hasOpenai ? 'opacity-50' : ''}`}>
|
|
125
|
+
<span className="flex items-center gap-1.5">
|
|
126
|
+
<span>{m.name}</span>
|
|
127
|
+
<CostBadge cost={m.cost} />
|
|
128
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
129
|
+
{!hasOpenai && <Key className="h-3 w-3 text-muted-foreground/50" />}
|
|
92
130
|
</span>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</SelectItem>
|
|
98
|
-
))}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
))}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
99
135
|
</SelectContent>
|
|
100
136
|
</Select>
|
|
101
137
|
);
|