mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03c83b0a2e | ||
![]() |
ae6a6e4064 | ||
![]() |
67483d08db | ||
![]() |
53268b528d | ||
![]() |
f9d5fc8282 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "crates/yachtpit"]
|
||||
path = crates/yachtpit
|
||||
url = https://github.com/seemueller-io/yachtpit.git
|
@@ -1,5 +1,5 @@
|
||||
# open-gsio
|
||||
|
||||
> Rewrite in-progress.
|
||||
[](https://github.com/geoffsee/open-gsio/actions/workflows/test.yml)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
</br>
|
||||
|
Submodule crates/yachtpit deleted from 348f20641c
@@ -10,7 +10,6 @@
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "packages/scripts/cleanup.sh",
|
||||
"restore:submodules": "rm -rf crates/yachtpit && (git rm --cached crates/yachtpit) && git submodule add --force https://github.com/seemueller-io/yachtpit.git crates/yachtpit ",
|
||||
"test:all": "bun run --filter='*' tests",
|
||||
"client:dev": "(cd packages/client && bun run dev)",
|
||||
"server:dev": "bun build:client && (cd packages/server && bun run dev)",
|
||||
|
@@ -24,68 +24,10 @@ export class ProviderRepository {
|
||||
};
|
||||
|
||||
static async getModelFamily(model: any, env: GenericEnv) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Looking up model "${model}"`);
|
||||
|
||||
const allModels = await env.KV_STORAGE.get('supportedModels');
|
||||
const models = JSON.parse(allModels);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Found ${models.length} total models in KV storage`);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.getModelFamily: Available model IDs:', models.map((m: ModelMeta) => m.id));
|
||||
|
||||
// First try exact match
|
||||
let modelData = models.filter((m: ModelMeta) => m.id === model);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Exact match attempt for "${model}" found ${modelData.length} results`);
|
||||
|
||||
// If no exact match, try to find by partial match (handle provider prefixes)
|
||||
if (modelData.length === 0) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Trying partial match for "${model}"`);
|
||||
modelData = models.filter((m: ModelMeta) => {
|
||||
// Check if the model ID ends with the requested model name
|
||||
// This handles cases like "accounts/fireworks/models/mixtral-8x22b-instruct" matching "mixtral-8x22b-instruct"
|
||||
const endsWithMatch = m.id.endsWith(model);
|
||||
const modelEndsWithStoredBase = model.endsWith(m.id.split('/').pop() || '');
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Checking "${m.id}" - endsWith: ${endsWithMatch}, modelEndsWithBase: ${modelEndsWithStoredBase}`);
|
||||
return endsWithMatch || modelEndsWithStoredBase;
|
||||
});
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Partial match found ${modelData.length} results`);
|
||||
}
|
||||
|
||||
// If still no match, try to find by the base model name (last part after /)
|
||||
if (modelData.length === 0) {
|
||||
const baseModelName = model.split('/').pop();
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Trying base name match for "${baseModelName}"`);
|
||||
modelData = models.filter((m: ModelMeta) => {
|
||||
const baseStoredName = m.id.split('/').pop();
|
||||
const matches = baseStoredName === baseModelName;
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Comparing base names "${baseStoredName}" === "${baseModelName}": ${matches}`);
|
||||
return matches;
|
||||
});
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Base name match found ${modelData.length} results`);
|
||||
}
|
||||
|
||||
const selectedProvider = modelData[0]?.provider;
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Final result for "${model}" -> provider: "${selectedProvider}"`);
|
||||
|
||||
if (modelData.length > 0) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.getModelFamily: Selected model data:', modelData[0]);
|
||||
} else {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: No matching model found for "${model}"`);
|
||||
}
|
||||
|
||||
return selectedProvider;
|
||||
const modelData = models.filter((m: ModelMeta) => m.id === model);
|
||||
return modelData[0].provider;
|
||||
}
|
||||
|
||||
static async getModelMeta(meta: any, env: GenericEnv) {
|
||||
@@ -99,19 +41,12 @@ export class ProviderRepository {
|
||||
}
|
||||
|
||||
setProviders(env: GenericEnv) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Starting provider detection');
|
||||
|
||||
const indicies = {
|
||||
providerName: 0,
|
||||
providerValue: 1,
|
||||
};
|
||||
const valueDelimiter = '_';
|
||||
const envKeys = Object.keys(env);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Environment keys ending with KEY:', envKeys.filter(key => key.endsWith('KEY')));
|
||||
|
||||
for (let i = 0; i < envKeys.length; i++) {
|
||||
if (envKeys.at(i)?.endsWith('KEY')) {
|
||||
const detectedProvider = envKeys
|
||||
@@ -120,15 +55,9 @@ export class ProviderRepository {
|
||||
.at(indicies.providerName)
|
||||
?.toLowerCase();
|
||||
const detectedProviderValue = env[envKeys.at(i) as string];
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Processing ${envKeys[i]} -> detected provider: "${detectedProvider}", has value: ${!!detectedProviderValue}`);
|
||||
|
||||
if (detectedProviderValue) {
|
||||
switch (detectedProvider) {
|
||||
case 'anthropic':
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Claude provider (anthropic)');
|
||||
this.#providers.push({
|
||||
name: 'claude',
|
||||
key: env.ANTHROPIC_API_KEY,
|
||||
@@ -136,8 +65,6 @@ export class ProviderRepository {
|
||||
});
|
||||
break;
|
||||
case 'gemini':
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Google provider (gemini)');
|
||||
this.#providers.push({
|
||||
name: 'google',
|
||||
key: env.GEMINI_API_KEY,
|
||||
@@ -145,8 +72,6 @@ export class ProviderRepository {
|
||||
});
|
||||
break;
|
||||
case 'cloudflare':
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Cloudflare provider');
|
||||
this.#providers.push({
|
||||
name: 'cloudflare',
|
||||
key: env.CLOUDFLARE_API_KEY,
|
||||
@@ -157,8 +82,6 @@ export class ProviderRepository {
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Adding default provider "${detectedProvider}"`);
|
||||
this.#providers.push({
|
||||
name: detectedProvider as SupportedProvider,
|
||||
key: env[envKeys[i] as string],
|
||||
@@ -166,14 +89,8 @@ export class ProviderRepository {
|
||||
ProviderRepository.OPENAI_COMPAT_ENDPOINTS[detectedProvider as SupportedProvider],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Skipping ${envKeys[i]} - no value provided`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Final configured providers (${this.#providers.length}):`, this.#providers.map(p => ({ name: p.name, endpoint: p.endpoint, hasKey: !!p.key })));
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
import ChatSdk from '../chat-sdk/chat-sdk.ts';
|
||||
import { mapControlAi, MapsTools } from '../tools/maps.ts';
|
||||
import { getWeather, WeatherTool } from '../tools/weather.ts';
|
||||
import { yachtpitAi, YachtpitTools } from '../tools/yachtpit.ts';
|
||||
import type { GenericEnv } from '../types';
|
||||
|
||||
export interface CommonProviderParams {
|
||||
@@ -38,14 +38,14 @@ export abstract class BaseChatProvider implements ChatStreamProvider {
|
||||
|
||||
const client = this.getOpenAIClient(param);
|
||||
|
||||
const tools = [WeatherTool, MapsTools];
|
||||
const tools = [WeatherTool, YachtpitTools];
|
||||
|
||||
const callFunction = async (name, args) => {
|
||||
if (name === 'get_weather') {
|
||||
return getWeather(args.latitude, args.longitude);
|
||||
}
|
||||
if (name === 'maps_control') {
|
||||
return mapControlAi({ action: args.action, value: args.value });
|
||||
if (name === 'ship_control') {
|
||||
return yachtpitAi({ action: args.action, value: args.value });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,7 +236,7 @@ export abstract class BaseChatProvider implements ChatStreamProvider {
|
||||
|
||||
// Process chunk normally for non-tool-call responses
|
||||
if (!chunk.choices[0]?.delta?.tool_calls) {
|
||||
// console.log('after-tool-call-chunk', chunk);
|
||||
console.log('after-tool-call-chunk', chunk);
|
||||
const shouldBreak = await this.processChunk(chunk, dataCallback);
|
||||
if (shouldBreak) {
|
||||
conversationComplete = true;
|
||||
|
@@ -15,10 +15,21 @@ export class FireworksAiChatProvider extends BaseChatProvider {
|
||||
let modelPrefix = 'accounts/fireworks/models/';
|
||||
if (param.model.toLowerCase().includes('yi-')) {
|
||||
modelPrefix = 'accounts/yi-01-ai/models/';
|
||||
} else if (param.model.toLowerCase().includes('/perplexity/')) {
|
||||
modelPrefix = 'accounts/perplexity/models/';
|
||||
} else if (param.model.toLowerCase().includes('/sentientfoundation/')) {
|
||||
modelPrefix = 'accounts/sentientfoundation/models/';
|
||||
} else if (param.model.toLowerCase().includes('/sentientfoundation-serverless/')) {
|
||||
modelPrefix = 'accounts/sentientfoundation-serverless/models/';
|
||||
} else if (param.model.toLowerCase().includes('/instacart/')) {
|
||||
modelPrefix = 'accounts/instacart/models/';
|
||||
}
|
||||
|
||||
const finalModelIdentifier = param.model.includes(modelPrefix)
|
||||
? param.model
|
||||
: `${modelPrefix}${param.model}`;
|
||||
console.log('using fireworks model', finalModelIdentifier);
|
||||
return {
|
||||
model: `${param.model}`,
|
||||
model: finalModelIdentifier,
|
||||
messages: safeMessages,
|
||||
stream: true,
|
||||
};
|
||||
|
@@ -1,111 +0,0 @@
|
||||
export interface MapsControlResult {
|
||||
message: string;
|
||||
status: 'success' | 'error';
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock interface for controlling a map.
|
||||
*/
|
||||
export const MapsTools = {
|
||||
type: 'function',
|
||||
|
||||
/**
|
||||
* Mock implementation of a maps control command.
|
||||
*/
|
||||
function: {
|
||||
name: 'maps_control',
|
||||
description:
|
||||
'Interface for controlling a web-rendered map to explore publicly available geospatial data',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['add_point', 'zoom_to', 'search_datasets', 'add_dataset', 'remove_dataset'],
|
||||
description: 'Action to perform on the geospatial map.',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Numeric value for the action, indicating a code for reference',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function mapControlAi(args: { action: string; value?: string }): Promise<MapsControlResult> {
|
||||
switch (args.action) {
|
||||
case 'add_point': {
|
||||
if (!args.value) {
|
||||
return Promise.resolve({
|
||||
status: 'error',
|
||||
message: 'Missing point coordinates or reference code.',
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: `Point added to map with reference: ${args.value}`,
|
||||
data: { pointId: args.value, action: 'add_point' },
|
||||
});
|
||||
}
|
||||
|
||||
case 'zoom_to': {
|
||||
if (!args.value) {
|
||||
return Promise.resolve({ status: 'error', message: 'Missing zoom target reference.' });
|
||||
}
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: `Map zoomed to: ${args.value}`,
|
||||
data: { target: args.value, action: 'zoom_to' },
|
||||
});
|
||||
}
|
||||
|
||||
case 'search_datasets': {
|
||||
const searchTerm = args.value || 'all';
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: `Searching datasets for: ${searchTerm}`,
|
||||
data: {
|
||||
searchTerm,
|
||||
action: 'search_datasets',
|
||||
results: [
|
||||
{ id: 'osm', name: 'OpenStreetMap', type: 'base_layer' },
|
||||
{ id: 'satellite', name: 'Satellite Imagery', type: 'base_layer' },
|
||||
{ id: 'maritime', name: 'Maritime Data', type: 'overlay' },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
case 'add_dataset': {
|
||||
if (!args.value) {
|
||||
return Promise.resolve({ status: 'error', message: 'Missing dataset reference.' });
|
||||
}
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: `Dataset added to map: ${args.value}`,
|
||||
data: { datasetId: args.value, action: 'add_dataset' },
|
||||
});
|
||||
}
|
||||
|
||||
case 'remove_dataset': {
|
||||
if (!args.value) {
|
||||
return Promise.resolve({ status: 'error', message: 'Missing dataset reference.' });
|
||||
}
|
||||
return Promise.resolve({
|
||||
status: 'success',
|
||||
message: `Dataset removed from map: ${args.value}`,
|
||||
data: { datasetId: args.value, action: 'remove_dataset' },
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return Promise.resolve({
|
||||
status: 'error',
|
||||
message: `Invalid action: ${args.action}. Valid actions are: add_point, zoom_to, search_datasets, add_dataset, remove_dataset`,
|
||||
});
|
||||
}
|
||||
}
|
68
packages/ai/src/tools/yachtpit.ts
Normal file
68
packages/ai/src/tools/yachtpit.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export interface ShipControlResult {
|
||||
message: string;
|
||||
status: 'success' | 'error';
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock interface for controlling a ship.
|
||||
*/
|
||||
export const YachtpitTools = {
|
||||
type: 'function',
|
||||
description: 'Interface for controlling a ship: set speed, change heading, report status, etc.',
|
||||
|
||||
/**
|
||||
* Mock implementation of a ship control command.
|
||||
*/
|
||||
function: {
|
||||
name: 'ship_control',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['set_speed', 'change_heading', 'report_status', 'stop'],
|
||||
description: 'Action to perform on the ship.',
|
||||
},
|
||||
value: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Numeric value for the action, such as speed (knots) or heading (degrees). Only required for set_speed and change_heading.',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function yachtpitAi(args: { action: string; value?: number }): Promise<ShipControlResult> {
|
||||
switch (args.action) {
|
||||
case 'set_speed':
|
||||
if (typeof args.value !== 'number') {
|
||||
return { status: 'error', message: 'Missing speed value.' };
|
||||
}
|
||||
return { status: 'success', message: `Speed set to ${args.value} knots.` };
|
||||
case 'change_heading':
|
||||
if (typeof args.value !== 'number') {
|
||||
return { status: 'error', message: 'Missing heading value.' };
|
||||
}
|
||||
return { status: 'success', message: `Heading changed to ${args.value} degrees.` };
|
||||
case 'report_status':
|
||||
// Return a simulated ship status
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Ship status reported.',
|
||||
data: {
|
||||
speed: 12,
|
||||
heading: 87,
|
||||
engine: 'nominal',
|
||||
position: { lat: 42.35, lon: -70.88 },
|
||||
},
|
||||
};
|
||||
case 'stop':
|
||||
return { status: 'success', message: 'Ship stopped.' };
|
||||
default:
|
||||
return { status: 'error', message: 'Invalid action.' };
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@
|
||||
"generate:sitemap": "bun ./scripts/generate_sitemap.js open-gsio.seemueller.workers.dev",
|
||||
"generate:robotstxt": "bun ./scripts/generate_robots_txt.js open-gsio.seemueller.workers.dev",
|
||||
"generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static",
|
||||
"generate:bevy:bundle": "bun scripts/generate-bevy-bundle.js",
|
||||
"generate:pwa:assets": "test ! -f public/pwa-64x64.png && pwa-assets-generator --preset minimal-2023 public/logo.png || echo 'PWA assets already exist'"
|
||||
},
|
||||
"exports": {
|
||||
|
@@ -1,196 +0,0 @@
|
||||
import { execSync, execFileSync } from 'node:child_process';
|
||||
import {
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
cpSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { resolve, dirname, join, basename } from 'node:path';
|
||||
|
||||
import { Logger } from 'tslog';
|
||||
const logger = new Logger({
|
||||
stdio: 'inherit',
|
||||
prettyLogTimeZone: 'local',
|
||||
type: 'pretty',
|
||||
stylePrettyLogs: true,
|
||||
prefix: ['\n'],
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
function main() {
|
||||
bundleCrate();
|
||||
cleanup();
|
||||
logger.info('🎉 yachtpit built successfully');
|
||||
}
|
||||
|
||||
const getRepoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
||||
const repoRoot = resolve(getRepoRoot);
|
||||
const publicDir = resolve(repoRoot, 'packages/client/public');
|
||||
const indexHtml = resolve(publicDir, 'index.html');
|
||||
|
||||
// Build the yachtpit project
|
||||
const buildCwd = resolve(repoRoot, 'crates/yachtpit/crates/yachtpit');
|
||||
logger.info(`🔨 Building in directory: ${buildCwd}`);
|
||||
|
||||
function needsRebuild() {
|
||||
const optimizedWasm = join(buildCwd, 'dist', 'yachtpit_bg.wasm_optimized');
|
||||
if (!existsSync(optimizedWasm)) return true;
|
||||
}
|
||||
|
||||
const NEEDS_REBUILD = needsRebuild();
|
||||
|
||||
function bundleCrate() {
|
||||
// ───────────── Build yachtpit project ───────────────────────────────────
|
||||
logger.info('🔨 Building yachtpit...');
|
||||
|
||||
logger.info(`📁 Repository root: ${repoRoot}`);
|
||||
|
||||
// Check if submodules need to be initialized
|
||||
const yachtpitPath = resolve(repoRoot, 'crates/yachtpit');
|
||||
logger.info(`📁 Yachtpit path: ${yachtpitPath}`);
|
||||
|
||||
if (!existsSync(yachtpitPath)) {
|
||||
logger.info('📦 Initializing submodules...');
|
||||
execSync('git submodule update --init --remote', { stdio: 'inherit' });
|
||||
} else {
|
||||
logger.info(`✅ Submodules already initialized at: ${yachtpitPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (NEEDS_REBUILD) {
|
||||
logger.info('🛠️ Changes detected — rebuilding yachtpit...');
|
||||
execSync('trunk build --release', { cwd: buildCwd, stdio: 'inherit' });
|
||||
logger.info('✅ Yachtpit built');
|
||||
} else {
|
||||
logger.info('⏩ No changes since last build — skipping yachtpit rebuild');
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to build yachtpit:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ───────────── Copy assets to public directory ──────────────────────────
|
||||
const yachtpitDistDir = join(buildCwd, 'dist');
|
||||
|
||||
logger.info(`📋 Copying assets to public directory...`);
|
||||
|
||||
// Remove existing yachtpit assets from public directory
|
||||
const skipRemoveOldAssets = false;
|
||||
|
||||
if (!skipRemoveOldAssets) {
|
||||
const existingAssets = readdirSync(publicDir).filter(
|
||||
file => file.startsWith('yachtpit') && (file.endsWith('.js') || file.endsWith('.wasm')),
|
||||
);
|
||||
|
||||
existingAssets.forEach(asset => {
|
||||
const assetPath = join(publicDir, asset);
|
||||
rmSync(assetPath, { force: true });
|
||||
logger.info(`🗑️ Removed old asset: ${assetPath}`);
|
||||
});
|
||||
} else {
|
||||
logger.warn('SKIPPING REMOVING OLD ASSETS');
|
||||
}
|
||||
|
||||
// Copy new assets from yachtpit/dist to public directory
|
||||
if (existsSync(yachtpitDistDir)) {
|
||||
logger.info(`📍Located yachtpit build: ${yachtpitDistDir}`);
|
||||
try {
|
||||
cpSync(yachtpitDistDir, publicDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
logger.info(`✅ Assets copied from ${yachtpitDistDir} to ${publicDir}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to copy assets:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Yachtpit dist directory not found at: ${yachtpitDistDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ───────────── locate targets ───────────────────────────────────────────
|
||||
const dstPath = join(publicDir, 'yachtpit.html');
|
||||
|
||||
// Regexes for the hashed filenames produced by most bundlers
|
||||
const JS_RE = /^yachtpit-[\da-f]{16}\.js$/i;
|
||||
const WASM_RE = /^yachtpit-[\da-f]{16}_bg\.wasm$/i;
|
||||
|
||||
// Always perform renaming of bundle files
|
||||
const files = readdirSync(publicDir);
|
||||
|
||||
// helper that doesn't explode if the target file is already present
|
||||
const safeRename = (from, to) => {
|
||||
if (!existsSync(from)) return;
|
||||
if (existsSync(to)) {
|
||||
logger.info(`ℹ️ ${to} already exists – removing and replacing.`);
|
||||
rmSync(to, { force: true });
|
||||
}
|
||||
renameSync(from, to);
|
||||
logger.info(`📝 Renamed: ${basename(from)} → ${basename(to)}`);
|
||||
};
|
||||
|
||||
files.forEach(f => {
|
||||
const fullPath = join(publicDir, f);
|
||||
if (JS_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit.js'));
|
||||
if (WASM_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit_bg.wasm'));
|
||||
});
|
||||
|
||||
// ───────────── patch markup inside HTML ─────────────────────────────────
|
||||
if (existsSync(indexHtml)) {
|
||||
logger.info(`📝 Patching HTML file: ${indexHtml}`);
|
||||
let html = readFileSync(indexHtml, 'utf8');
|
||||
|
||||
html = html
|
||||
.replace(/yachtpit-[\da-f]{16}\.js/gi, 'yachtpit.js')
|
||||
.replace(/yachtpit-[\da-f]{16}_bg\.wasm/gi, 'yachtpit_bg.wasm');
|
||||
|
||||
writeFileSync(indexHtml, html, 'utf8');
|
||||
|
||||
// ───────────── rename HTML entrypoint ─────────────────────────────────
|
||||
if (basename(indexHtml) !== 'yachtpit.html') {
|
||||
logger.info(`📝 Renaming HTML file: ${indexHtml} → ${dstPath}`);
|
||||
// Remove existing yachtpit.html if it exists
|
||||
if (existsSync(dstPath)) {
|
||||
rmSync(dstPath, { force: true });
|
||||
}
|
||||
renameSync(indexHtml, dstPath);
|
||||
}
|
||||
} else {
|
||||
logger.info(`⚠️ ${indexHtml} not found – skipping HTML processing.`);
|
||||
}
|
||||
optimizeWasmSize();
|
||||
}
|
||||
|
||||
function optimizeWasmSize() {
|
||||
logger.info('🔨 Checking WASM size...');
|
||||
const wasmPath = resolve(publicDir, 'yachtpit_bg.wasm');
|
||||
const fileSize = statSync(wasmPath).size;
|
||||
const sizeInMb = fileSize / (1024 * 1024);
|
||||
|
||||
if (sizeInMb > 30) {
|
||||
logger.info(`WASM size is ${sizeInMb.toFixed(2)}MB, optimizing...`);
|
||||
execFileSync('wasm-opt', ['-Oz', '-o', wasmPath, wasmPath], {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
logger.info(`✅ WASM size optimized`);
|
||||
} else {
|
||||
logger.info(
|
||||
`⏩ Skipping WASM optimization, size (${sizeInMb.toFixed(2)}MB) is under 30MB threshold`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
logger.info('Running cleanup...');
|
||||
rmSync(indexHtml, { force: true });
|
||||
const creditsDir = resolve(`${repoRoot}/packages/client/public`, 'credits');
|
||||
rmSync(creditsDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
main();
|
@@ -8,7 +8,6 @@ import {
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Spinner,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useOutsideClick,
|
||||
@@ -42,38 +41,19 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
||||
|
||||
const [controlledOpen, setControlledOpen] = useState<boolean>(false);
|
||||
const [supportedModels, setSupportedModels] = useState<any[]>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
setControlledOpen(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingModels(true);
|
||||
fetch('/api/models')
|
||||
.then(response => response.json())
|
||||
.then(models => {
|
||||
setSupportedModels(models);
|
||||
|
||||
// Update the ModelStore with supported models
|
||||
const modelIds = models.map((model: any) => model.id);
|
||||
clientChatStore.setSupportedModels(modelIds);
|
||||
|
||||
// If no model is currently selected or the current model is not in the list,
|
||||
// select a random model from the available ones
|
||||
if (!clientChatStore.model || !modelIds.includes(clientChatStore.model)) {
|
||||
if (models.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * models.length);
|
||||
const randomModel = models[randomIndex];
|
||||
clientChatStore.setModel(randomModel.id);
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoadingModels(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Could not fetch models: ', err);
|
||||
setIsLoadingModels(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -128,8 +108,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
bg="text.accent"
|
||||
icon={isLoadingModels ? <Spinner size="sm" /> : <Settings size={20} />}
|
||||
isDisabled={isDisabled || isLoadingModels}
|
||||
icon={<Settings size={20} />}
|
||||
isDisabled={isDisabled}
|
||||
aria-label="Settings"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.2)' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
@@ -138,8 +118,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
||||
) : (
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={isLoadingModels ? <Spinner size="sm" /> : <ChevronDown size={16} />}
|
||||
isDisabled={isDisabled || isLoadingModels}
|
||||
rightIcon={<ChevronDown size={16} />}
|
||||
isDisabled={isDisabled}
|
||||
variant="ghost"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
@@ -148,7 +128,7 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
||||
{...MsM_commonButtonStyles}
|
||||
>
|
||||
<Text noOfLines={1} maxW="100px" fontSize="sm">
|
||||
{isLoadingModels ? 'Loading...' : clientChatStore.model}
|
||||
{clientChatStore.model}
|
||||
</Text>
|
||||
</MenuButton>
|
||||
)}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useComponent } from '../contexts/ComponentContext.tsx';
|
||||
|
||||
@@ -11,23 +11,6 @@ export const LandingComponent: React.FC = () => {
|
||||
const [mapActive, setMapActive] = useState(true);
|
||||
const [aiActive, setAiActive] = useState(false);
|
||||
|
||||
const appCtlState = `app-ctl-state`;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const value = localStorage.getItem(appCtlState);
|
||||
if (value) {
|
||||
const parsed = JSON.parse(value);
|
||||
setIntensity(parsed.intensity);
|
||||
setMapActive(parsed.mapActive);
|
||||
setAiActive(parsed.aiActive);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// create a hook for saving the state as a json object when it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem(appCtlState, JSON.stringify({ intensity, mapActive, aiActive }));
|
||||
});
|
||||
|
||||
const component = useComponent();
|
||||
const { setEnabledComponent } = component;
|
||||
|
||||
@@ -38,14 +21,12 @@ export const LandingComponent: React.FC = () => {
|
||||
if (aiActive) {
|
||||
setEnabledComponent('ai');
|
||||
}
|
||||
}, [mapActive, aiActive, setEnabledComponent]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box as="section" bg="background.primary" overflow="hidden">
|
||||
<Box position="fixed" right={0} maxWidth="300px" minWidth="200px" zIndex={1000}>
|
||||
<Tweakbox
|
||||
id="app-tweaker"
|
||||
persist={true}
|
||||
sliders={{
|
||||
intensity: {
|
||||
value: intensity,
|
||||
@@ -87,6 +68,7 @@ export const LandingComponent: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/*<BevyScene speed={speed} intensity={intensity} glow={glow} visible={bevyScene} />*/}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import ReactMap from 'react-map-gl/mapbox'; // ↔ v5+ uses this import path
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Box, Button, HStack, Input } from '@chakra-ui/react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import clientChatStore from '../../stores/ClientChatStore.ts';
|
||||
import { Box, HStack, Button, Input, Center } from '@chakra-ui/react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import MapNext from './MapNext.tsx';
|
||||
|
||||
@@ -31,175 +30,17 @@ interface AuthParams {
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
export type Layer = { name: string; value: string };
|
||||
export type Layers = Layer[];
|
||||
|
||||
// public key
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
const layers = [
|
||||
{ name: 'Bathymetry', value: 'mapbox://styles/geoffsee/cmd1qz39x01ga01qv5acea02y' },
|
||||
{ name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
|
||||
];
|
||||
|
||||
function LayerSelector(props: { onClick: (e) => Promise<void> }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Button colorScheme="blue" size="sm" variant="solid" onClick={() => setIsOpen(!isOpen)}>
|
||||
Layer
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
w="200px"
|
||||
bg="background.secondary"
|
||||
boxShadow="md"
|
||||
zIndex={2}
|
||||
>
|
||||
{layers.map(layer => (
|
||||
<Box
|
||||
id={layer.value}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: 'whiteAlpha.200' }}
|
||||
onClick={async e => {
|
||||
setIsOpen(false);
|
||||
await props.onClick(e);
|
||||
}}
|
||||
>
|
||||
{layer.name}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Map(props: { visible: boolean }) {
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedLayer, setSelectedLayer] = useState(layers[0]);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
|
||||
// const handleSearchClick = useCallback(async () => {
|
||||
// console
|
||||
// }, []);
|
||||
//
|
||||
|
||||
async function selectSearchResult({ lat, lon }) {
|
||||
// clientChatStore.mapState.latitude = searchResult.lat;
|
||||
// clientChatStore.mapState.longitude = searchResult.lon;
|
||||
await clientChatStore.setMapView(lon, lat, 15);
|
||||
}
|
||||
|
||||
async function handleSc(e) {
|
||||
if (isSearchOpen && searchInput.length > 1) {
|
||||
try {
|
||||
console.log(`trying to geocode ${searchInput}`);
|
||||
const geocode = await fetch('https://geocode.geoffsee.com', {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify({
|
||||
location: searchInput,
|
||||
}),
|
||||
});
|
||||
const coordinates = await geocode.json();
|
||||
const { lat, lon } = coordinates;
|
||||
console.log(`got geocode coordinates: ${coordinates}`);
|
||||
setSearchResults([{ lat, lon }]);
|
||||
} catch (e) {
|
||||
// continue without
|
||||
}
|
||||
} else {
|
||||
setIsSearchOpen(!isSearchOpen);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(selectedLayer);
|
||||
}, [selectedLayer]);
|
||||
|
||||
function handleLayerChange(e) {
|
||||
setSelectedLayer(layers.find(layer => layer.value === e.target.id));
|
||||
}
|
||||
|
||||
return (
|
||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||
<Box position={'absolute'} top={0} w="100%" h={'100vh'} overflow="hidden">
|
||||
<Box position={'absolute'} top={0} w="100vw" h={'100vh'} overflow="hidden">
|
||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||
|
||||
<HStack position="relative" zIndex={1}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button size="sm" variant="solid" onClick={handleSc} mr={2}>
|
||||
Search
|
||||
</Button>
|
||||
{isSearchOpen && (
|
||||
<Box
|
||||
w="200px"
|
||||
transition="all 0.3s"
|
||||
transform={`translateX(${isSearchOpen ? '0' : '100%'})`}
|
||||
background="background.secondary"
|
||||
opacity={isSearchOpen ? 1 : 0}
|
||||
color="white"
|
||||
>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
size="sm"
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
color="white"
|
||||
bg="background.secondary"
|
||||
border="none"
|
||||
borderRadius="0"
|
||||
_focus={{
|
||||
outline: 'none',
|
||||
}}
|
||||
_placeholder={{
|
||||
color: '#d1cfcf',
|
||||
}}
|
||||
/>
|
||||
{searchResults.length > 0 && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
w="200px"
|
||||
bg="background.secondary"
|
||||
boxShadow="md"
|
||||
zIndex={2}
|
||||
>
|
||||
{searchResults.map((result, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: 'whiteAlpha.200' }}
|
||||
onClick={async () => {
|
||||
// setSearchInput(result);
|
||||
console.log(`selecting result ${result.lat}, ${result.lon}`);
|
||||
await selectSearchResult(result);
|
||||
setSearchResults([]);
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
{`${result.lat}, ${result.lon}`}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<LayerSelector onClick={handleLayerChange} />
|
||||
</HStack>
|
||||
<MapNext mapboxPublicKey={atob(key)} visible={props.visible} layer={selectedLayer} />
|
||||
<MapNext mapboxPublicKey={atob(key)} />
|
||||
{/*<Map*/}
|
||||
{/* mapboxAccessToken={atob(key)}*/}
|
||||
{/* initialViewState={mapView}*/}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Map, {
|
||||
FullscreenControl,
|
||||
GeolocateControl,
|
||||
@@ -8,42 +7,27 @@ import Map, {
|
||||
NavigationControl,
|
||||
Popup,
|
||||
ScaleControl,
|
||||
Source,
|
||||
} from 'react-map-gl/mapbox';
|
||||
|
||||
import clientChatStore from '../../stores/ClientChatStore';
|
||||
|
||||
import type { Layer } from './Map.tsx';
|
||||
import PORTS from './nautical-base-data.json';
|
||||
import Pin from './pin';
|
||||
|
||||
function MapNextComponent(
|
||||
props: any = { mapboxPublicKey: '', visible: true, layer: {} as Layer } as any,
|
||||
) {
|
||||
export default function MapNext(props: any = { mapboxPublicKey: '' } as any) {
|
||||
const [popupInfo, setPopupInfo] = useState(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [isTokenLoading, setIsTokenLoading] = useState(false);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
const mapRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setAuthenticated(true);
|
||||
setIsTokenLoading(false);
|
||||
}, []);
|
||||
|
||||
// Handle map resize when component becomes visible
|
||||
useEffect(() => {
|
||||
if (props.visible && mapRef.current) {
|
||||
// Small delay to ensure the container is fully visible
|
||||
const timer = setTimeout(() => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.resize();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [props.visible]);
|
||||
const [mapView, setMapView] = useState({
|
||||
longitude: -122.4,
|
||||
latitude: 37.8,
|
||||
zoom: 14,
|
||||
});
|
||||
|
||||
const handleNavigationClick = useCallback(async () => {
|
||||
console.log('handling navigation in map');
|
||||
@@ -55,10 +39,7 @@ function MapNextComponent(
|
||||
|
||||
const handleMapViewChange = useCallback(async (evt: any) => {
|
||||
const { longitude, latitude, zoom } = evt.viewState;
|
||||
clientChatStore.setMapView(longitude, latitude, zoom);
|
||||
// setMapView({ longitude, latitude, zoom });
|
||||
|
||||
// Update the store with the new view state
|
||||
setMapView({ longitude, latitude, zoom });
|
||||
}, []);
|
||||
|
||||
const pins = useMemo(
|
||||
@@ -117,19 +98,14 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
||||
{/* </Button>*/}
|
||||
{/*</HStack>*/}
|
||||
<Map
|
||||
ref={mapRef}
|
||||
initialViewState={{
|
||||
latitude: clientChatStore.mapState.latitude,
|
||||
longitude: clientChatStore.mapState.longitude,
|
||||
zoom: clientChatStore.mapState.zoom,
|
||||
bearing: clientChatStore.mapState.bearing,
|
||||
pitch: clientChatStore.mapState.pitch,
|
||||
latitude: 40,
|
||||
longitude: -100,
|
||||
zoom: 3.5,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
}}
|
||||
viewState={clientChatStore.mapState}
|
||||
onMove={handleMapViewChange}
|
||||
terrain={{ source: 'mapbox-dem', exaggeration: 1.5 }}
|
||||
maxPitch={85}
|
||||
mapStyle={props.layer.value}
|
||||
mapStyle="mapbox://styles/geoffsee/cmd1qz39x01ga01qv5acea02y"
|
||||
attributionControl={false}
|
||||
mapboxAccessToken={props.mapboxPublicKey}
|
||||
style={{
|
||||
@@ -142,13 +118,6 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Source
|
||||
id="mapbox-dem"
|
||||
type="raster-dem"
|
||||
url="mapbox://mapbox.mapbox-terrain-dem-v1"
|
||||
tileSize={512}
|
||||
maxzoom={14}
|
||||
/>
|
||||
<GeolocateControl position="top-left" style={{ marginTop: '6rem' }} />
|
||||
<FullscreenControl position="top-left" />
|
||||
<NavigationControl position="top-left" />
|
||||
@@ -201,6 +170,3 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const MapNext = observer(MapNextComponent);
|
||||
export default MapNext;
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface SliderControl {
|
||||
value: number;
|
||||
@@ -34,8 +34,6 @@ interface SwitchControl {
|
||||
}
|
||||
|
||||
interface TweakboxProps {
|
||||
id: string;
|
||||
persist: boolean;
|
||||
sliders: {
|
||||
speed: SliderControl;
|
||||
intensity: SliderControl;
|
||||
@@ -46,7 +44,7 @@ interface TweakboxProps {
|
||||
} & Record<string, SwitchControl>;
|
||||
}
|
||||
|
||||
const Tweakbox = observer(({ id, persist, sliders, switches }: TweakboxProps) => {
|
||||
const Tweakbox = observer(({ sliders, switches }: TweakboxProps) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Box, useMediaQuery } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import Chat from '../../components/chat/Chat.tsx';
|
||||
@@ -21,11 +21,10 @@ export default function IndexPage() {
|
||||
|
||||
const component = useComponent();
|
||||
|
||||
const mediaQuery = useMediaQuery();
|
||||
|
||||
return (
|
||||
<Box height="100%" width="100%">
|
||||
<LandingComponent />
|
||||
|
||||
<Box
|
||||
display={component.enabledComponent === 'ai' ? undefined : 'none'}
|
||||
width="100%"
|
||||
@@ -37,8 +36,8 @@ export default function IndexPage() {
|
||||
</Box>
|
||||
<Box
|
||||
display={component.enabledComponent === 'gpsmap' ? undefined : 'none'}
|
||||
width={{ base: '100%', md: '100%' }}
|
||||
height={{ base: '100%', md: '100%' }}
|
||||
width="100%"
|
||||
height="100%"
|
||||
padding={'unset'}
|
||||
>
|
||||
<ReactMap visible={component.enabledComponent === 'gpsmap'} />
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
'/': { sidebarLabel: 'Home', heroLabel: 'gsio' },
|
||||
'/': { sidebarLabel: 'Home', heroLabel: 'o-gsio' },
|
||||
'/connect': { sidebarLabel: 'Connect', heroLabel: 'connect' },
|
||||
'/privacy-policy': {
|
||||
sidebarLabel: '',
|
||||
|
@@ -3,14 +3,13 @@
|
||||
// ---------------------------
|
||||
import { types, type Instance } from 'mobx-state-tree';
|
||||
|
||||
import { MapStore } from './MapStore';
|
||||
import { MessagesStore } from './MessagesStore';
|
||||
import { ModelStore } from './ModelStore';
|
||||
import { StreamStore } from './StreamStore';
|
||||
import { UIStore } from './UIStore';
|
||||
|
||||
export const ClientChatStore = types
|
||||
.compose(MessagesStore, UIStore, ModelStore, StreamStore, MapStore)
|
||||
.compose(MessagesStore, UIStore, ModelStore, StreamStore)
|
||||
.named('ClientChatStore');
|
||||
|
||||
const clientChatStore = ClientChatStore.create();
|
||||
|
@@ -1,122 +0,0 @@
|
||||
import { types, type Instance } from 'mobx-state-tree';
|
||||
|
||||
export interface MapControlCommand {
|
||||
action: string;
|
||||
value?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export const MapStore = types
|
||||
.model('MapStore', {
|
||||
// Current map view state
|
||||
// 37°47'21"N 122°23'52"W
|
||||
longitude: types.optional(types.number, -87.6319),
|
||||
latitude: types.optional(types.number, 41.883415),
|
||||
zoom: types.optional(types.number, 14.5),
|
||||
bearing: types.optional(types.number, 15.165878375019094),
|
||||
pitch: types.optional(types.number, 45),
|
||||
// Map control state
|
||||
isControlActive: types.optional(types.boolean, false),
|
||||
})
|
||||
.volatile(self => ({
|
||||
// Store pending map commands from AI
|
||||
pendingCommands: [] as MapControlCommand[],
|
||||
// 41.88341413374059-87.630091075785714.57273962016686450
|
||||
mapState: {
|
||||
latitude: self.latitude,
|
||||
longitude: self.longitude,
|
||||
zoom: self.zoom,
|
||||
bearing: self.bearing,
|
||||
pitch: self.pitch,
|
||||
} as any,
|
||||
}))
|
||||
.actions(self => ({
|
||||
// Update map view state
|
||||
setMapView(longitude: number, latitude: number, zoom: number) {
|
||||
console.log(latitude, longitude, zoom, self.mapState.pitch, self.mapState.bearing);
|
||||
self.longitude = longitude;
|
||||
self.latitude = latitude;
|
||||
self.zoom = zoom;
|
||||
|
||||
// Also update the mapState object to keep it in sync
|
||||
self.mapState = {
|
||||
...self.mapState,
|
||||
longitude,
|
||||
latitude,
|
||||
zoom,
|
||||
};
|
||||
},
|
||||
|
||||
// Handle map control commands from AI
|
||||
executeMapCommand(command: MapControlCommand) {
|
||||
console.log('[DEBUG_LOG] Executing map command:', command);
|
||||
|
||||
switch (command.action) {
|
||||
case 'zoom_to': {
|
||||
if (command.data?.target) {
|
||||
// For now, we'll implement a simple zoom behavior
|
||||
// In a real implementation, this could parse coordinates or location names
|
||||
const zoomLevel = 10; // Default zoom level for zoom_to commands
|
||||
self.zoom = zoomLevel;
|
||||
console.log('[DEBUG_LOG] Zoomed to level:', zoomLevel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'add_point': {
|
||||
if (command.data?.pointId) {
|
||||
console.log('[DEBUG_LOG] Adding point:', command.data.pointId);
|
||||
// Point addition logic would go here
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'add_dataset':
|
||||
case 'remove_dataset': {
|
||||
if (command.data?.datasetId) {
|
||||
console.log('[DEBUG_LOG] Dataset operation:', command.action, command.data.datasetId);
|
||||
// Dataset management logic would go here
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'search_datasets': {
|
||||
console.log('[DEBUG_LOG] Searching datasets:', command.data?.searchTerm);
|
||||
// Dataset search logic would go here
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn('[DEBUG_LOG] Unknown map command:', command.action);
|
||||
}
|
||||
|
||||
self.isControlActive = true;
|
||||
|
||||
// Clear the command after a short delay
|
||||
setTimeout(() => {
|
||||
self.isControlActive = false;
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// Add a command to the pending queue
|
||||
addPendingCommand(command: MapControlCommand) {
|
||||
self.pendingCommands.push(command);
|
||||
},
|
||||
|
||||
// Process all pending commands
|
||||
processPendingCommands() {
|
||||
while (self.pendingCommands.length > 0) {
|
||||
const command = self.pendingCommands.shift();
|
||||
if (command) {
|
||||
this.executeMapCommand(command);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Clear all pending commands
|
||||
clearPendingCommands() {
|
||||
self.pendingCommands.splice(0);
|
||||
},
|
||||
}));
|
||||
|
||||
export type IMapStore = Instance<typeof MapStore>;
|
@@ -2,8 +2,6 @@ import { flow, getParent, type Instance, types } from 'mobx-state-tree';
|
||||
|
||||
import Message, { batchContentUpdate } from '../models/Message';
|
||||
|
||||
import clientChatStore from './ClientChatStore.ts';
|
||||
import type { MapControlCommand } from './MapStore';
|
||||
import type { RootDeps } from './RootDeps.ts';
|
||||
import UserOptionsStore from './UserOptionsStore';
|
||||
|
||||
@@ -94,30 +92,6 @@ export const StreamStore = types
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle tool call responses
|
||||
if (parsed.type === 'tool_result') {
|
||||
console.log('[DEBUG_LOG] Received tool result:', parsed);
|
||||
|
||||
// Check if this is a map control tool call
|
||||
if (parsed.tool_name === 'maps_control' && parsed.result?.data) {
|
||||
const mapCommand: MapControlCommand = {
|
||||
action: parsed.result.data.action,
|
||||
value: parsed.args?.value,
|
||||
data: parsed.result.data,
|
||||
};
|
||||
|
||||
console.log('[DEBUG_LOG] Processing map command:', mapCommand);
|
||||
|
||||
// Execute the map command through the store
|
||||
if ('executeMapCommand' in root) {
|
||||
(root as any).executeMapCommand(mapCommand);
|
||||
} else {
|
||||
console.warn('[DEBUG_LOG] MapStore not available in root');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the last message
|
||||
const lastMessage = root.items[root.items.length - 1];
|
||||
|
||||
@@ -178,4 +152,4 @@ export const StreamStore = types
|
||||
return { sendMessage, stopIncomingMessage, cleanup, setEventSource, setStreamId };
|
||||
});
|
||||
|
||||
export type IStreamStore = Instance<typeof StreamStore>;
|
||||
export interface IStreamStore extends Instance<typeof StreamStore> {}
|
||||
|
@@ -161,19 +161,29 @@ const ChatService = types
|
||||
|
||||
const openai = new OpenAI({ apiKey: provider.key, baseURL: provider.endpoint });
|
||||
|
||||
// 2‑a. List models
|
||||
const basicFilters = (model: any) => {
|
||||
return (
|
||||
!model.id.includes('whisper') &&
|
||||
!model.id.includes('flux') &&
|
||||
!model.id.includes('ocr') &&
|
||||
!model.id.includes('tts') &&
|
||||
!model.id.includes('guard')
|
||||
);
|
||||
}; // 2‑a. List models
|
||||
try {
|
||||
const listResp: any = yield openai.models.list(); // <‑‑ async
|
||||
const models = 'data' in listResp ? listResp.data : listResp;
|
||||
|
||||
providerModels.set(
|
||||
provider.name,
|
||||
models.filter(
|
||||
(mdl: any) =>
|
||||
!mdl.id.includes('whisper') &&
|
||||
!mdl.id.includes('tts') &&
|
||||
!mdl.id.includes('guard'),
|
||||
),
|
||||
models.filter((mdl: any) => {
|
||||
if ('supports_chat' in mdl && mdl.supports_chat) {
|
||||
return basicFilters(mdl);
|
||||
} else if ('supports_chat' in mdl && !mdl.supports_chat) {
|
||||
return false;
|
||||
}
|
||||
return basicFilters(mdl);
|
||||
}),
|
||||
);
|
||||
|
||||
// 2‑b. Retrieve metadata
|
||||
@@ -183,7 +193,7 @@ const ChatService = types
|
||||
modelMeta.set(mdl.id, { ...mdl, ...meta });
|
||||
} catch (err) {
|
||||
// logger.error(`Metadata fetch failed for ${mdl.id}`, err);
|
||||
modelMeta.set(mdl.id, { provider: provider.name, ...mdl });
|
||||
modelMeta.set(mdl.id, { provider: provider.name, mdl });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -277,23 +287,8 @@ const ChatService = types
|
||||
}) {
|
||||
const { streamConfig, streamParams, controller, encoder, streamId } = params;
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Processing model "${streamConfig.model}" for stream ${streamId}`,
|
||||
);
|
||||
|
||||
const modelFamily = await ProviderRepository.getModelFamily(streamConfig.model, self.env);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Detected model family "${modelFamily}" for model "${streamConfig.model}"`,
|
||||
);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
'[DEBUG_LOG] ChatService.runModelHandler: Available model handlers:',
|
||||
Object.keys(modelHandlers),
|
||||
);
|
||||
|
||||
const useModelHandler = () => {
|
||||
// @ts-expect-error - language server does not have enough information to validate modelFamily as an indexer for modelHandlers
|
||||
return modelHandlers[modelFamily];
|
||||
@@ -302,28 +297,9 @@ const ChatService = types
|
||||
const handler = useModelHandler();
|
||||
|
||||
if (handler) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Found handler for model family "${modelFamily}"`,
|
||||
);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Calling handler for model "${streamConfig.model}" with maxTokens: ${streamParams.maxTokens}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await handler(streamParams, Common.Utils.handleStreamData(controller, encoder));
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Successfully completed handler for model "${streamConfig.model}"`,
|
||||
);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Handler error for model "${streamConfig.model}":`,
|
||||
error.message,
|
||||
);
|
||||
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
if (
|
||||
@@ -352,80 +328,11 @@ const ChatService = types
|
||||
);
|
||||
}
|
||||
if (message.includes('404')) {
|
||||
// Try to find a fallback model from the same provider
|
||||
try {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Model "${streamConfig.model}" not found, attempting fallback`,
|
||||
);
|
||||
|
||||
const allModels = await self.env.KV_STORAGE.get('supportedModels');
|
||||
const models = JSON.parse(allModels);
|
||||
|
||||
// Find all models from the same provider
|
||||
const sameProviderModels = models.filter(
|
||||
(m: any) => m.provider === modelFamily && m.id !== streamConfig.model,
|
||||
);
|
||||
|
||||
if (sameProviderModels.length > 0) {
|
||||
// Try the first available model from the same provider
|
||||
const fallbackModel = sameProviderModels[0];
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Trying fallback model "${fallbackModel.id}" from provider "${modelFamily}"`,
|
||||
);
|
||||
|
||||
// Update streamParams with the fallback model
|
||||
const fallbackStreamParams = { ...streamParams, model: fallbackModel.id };
|
||||
|
||||
// Try the fallback model
|
||||
await handler(
|
||||
fallbackStreamParams,
|
||||
Common.Utils.handleStreamData(controller, encoder),
|
||||
);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Successfully completed handler with fallback model "${fallbackModel.id}"`,
|
||||
);
|
||||
return; // Success with fallback
|
||||
} else {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: No fallback models available for provider "${modelFamily}"`,
|
||||
);
|
||||
}
|
||||
} catch (fallbackError: any) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: Fallback attempt failed:`,
|
||||
fallbackError.message,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ClientError(
|
||||
`Model not found or unavailable. Please try a different model.`,
|
||||
404,
|
||||
{
|
||||
model: streamConfig.model,
|
||||
},
|
||||
);
|
||||
console.log(message);
|
||||
throw new ClientError(`Something went wrong, try again.`, 404, {});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.runModelHandler: No handler found for model family "${modelFamily}" (model: "${streamConfig.model}")`,
|
||||
);
|
||||
throw new ClientError(
|
||||
`No handler available for model family "${modelFamily}". Model: "${streamConfig.model}"`,
|
||||
500,
|
||||
{
|
||||
model: streamConfig.model,
|
||||
modelFamily: modelFamily,
|
||||
availableHandlers: Object.keys(modelHandlers),
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -437,27 +344,11 @@ const ChatService = types
|
||||
}) {
|
||||
const { streamId, streamConfig, savedStreamConfig, durableObject } = params;
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.createSseReadableStream: Creating stream ${streamId} for model "${streamConfig.model}"`,
|
||||
);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(`[DEBUG_LOG] ChatService.createSseReadableStream: Stream config:`, {
|
||||
model: streamConfig.model,
|
||||
systemPrompt: streamConfig.systemPrompt?.substring(0, 100) + '...',
|
||||
messageCount: streamConfig.messages?.length,
|
||||
});
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.createSseReadableStream: Starting stream processing for ${streamId}`,
|
||||
);
|
||||
|
||||
const dynamicContext = Schema.Message.create(streamConfig.preprocessedContext);
|
||||
|
||||
// Process the stream data using the appropriate handler
|
||||
@@ -467,16 +358,6 @@ const ChatService = types
|
||||
durableObject,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.createSseReadableStream: Created stream params for ${streamId}:`,
|
||||
{
|
||||
model: streamParams.model,
|
||||
maxTokens: streamParams.maxTokens,
|
||||
messageCount: streamParams.messages?.length,
|
||||
},
|
||||
);
|
||||
|
||||
await self.runModelHandler({
|
||||
streamConfig,
|
||||
streamParams,
|
||||
@@ -485,11 +366,6 @@ const ChatService = types
|
||||
streamId,
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.createSseReadableStream: Error in stream ${streamId}:`,
|
||||
error,
|
||||
);
|
||||
console.error(`chatService::handleSseStream::${streamId}::Error`, error);
|
||||
|
||||
if (error instanceof ClientError) {
|
||||
@@ -511,10 +387,6 @@ const ChatService = types
|
||||
controller.close();
|
||||
} finally {
|
||||
try {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.createSseReadableStream: Closing stream ${streamId}`,
|
||||
);
|
||||
controller.close();
|
||||
} catch (_) {
|
||||
// Ignore errors when closing the controller, as it might already be closed
|
||||
@@ -527,53 +399,21 @@ const ChatService = types
|
||||
handleSseStream: flow(function* (
|
||||
streamId: string,
|
||||
): Generator<Promise<string>, Response, unknown> {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Handling SSE stream request for ${streamId}`,
|
||||
);
|
||||
|
||||
// Check if a stream is already active for this ID
|
||||
if (self.activeStreams.has(streamId)) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Stream ${streamId} already active, returning 409`,
|
||||
);
|
||||
return new Response('Stream already active', { status: 409 });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Retrieving stream configuration for ${streamId}`,
|
||||
);
|
||||
|
||||
// Retrieve the stream configuration from the durable object
|
||||
const objectId = self.env.SERVER_COORDINATOR.idFromName('stream-index');
|
||||
const durableObject = self.env.SERVER_COORDINATOR.get(objectId);
|
||||
const savedStreamConfig: any = yield durableObject.getStreamData(streamId);
|
||||
|
||||
if (!savedStreamConfig) {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: No stream configuration found for ${streamId}, returning 404`,
|
||||
);
|
||||
return new Response('Stream not found', { status: 404 });
|
||||
}
|
||||
|
||||
const streamConfig = JSON.parse(savedStreamConfig);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Retrieved stream config for ${streamId}:`,
|
||||
{
|
||||
model: streamConfig.model,
|
||||
messageCount: streamConfig.messages?.length,
|
||||
systemPrompt: streamConfig.systemPrompt?.substring(0, 100) + '...',
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Creating SSE readable stream for ${streamId}`,
|
||||
);
|
||||
|
||||
const stream = self.createSseReadableStream({
|
||||
streamId,
|
||||
@@ -585,37 +425,18 @@ const ChatService = types
|
||||
// Use `tee()` to create two streams: one for processing and one for the response
|
||||
const [processingStream, responseStream] = stream.tee();
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Setting active stream for ${streamId}`,
|
||||
);
|
||||
|
||||
self.setActiveStream(streamId, {
|
||||
...streamConfig,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Setting up processing stream pipeline for ${streamId}`,
|
||||
);
|
||||
|
||||
processingStream.pipeTo(
|
||||
new WritableStream({
|
||||
close() {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Processing stream closed for ${streamId}, removing active stream`,
|
||||
);
|
||||
self.removeActiveStream(streamId);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
console.log(
|
||||
`[DEBUG_LOG] ChatService.handleSseStream: Returning response stream for ${streamId}`,
|
||||
);
|
||||
|
||||
// Return the second stream as the response
|
||||
return new Response(responseStream, {
|
||||
headers: {
|
||||
|
Reference in New Issue
Block a user