mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
chat + maps + ai + tools
This commit is contained in:

committed by
Geoff Seemueller

parent
48655474e3
commit
818e0e672a
@@ -1,7 +1,8 @@
|
||||
import { OpenAI } from 'openai';
|
||||
|
||||
import ChatSdk from '../chat-sdk/chat-sdk.ts';
|
||||
import { WeatherTool } from '../tools/basic.ts';
|
||||
import { getWeather, WeatherTool } from '../tools/weather.ts';
|
||||
import { yachtpitAi, YachtpitTools } from '../tools/yachtpit.ts';
|
||||
import type { GenericEnv } from '../types';
|
||||
|
||||
export interface CommonProviderParams {
|
||||
@@ -37,15 +38,14 @@ export abstract class BaseChatProvider implements ChatStreamProvider {
|
||||
|
||||
const client = this.getOpenAIClient(param);
|
||||
|
||||
const tools = [WeatherTool];
|
||||
|
||||
const getCurrentTemp = (location: string) => {
|
||||
return '20C';
|
||||
};
|
||||
const tools = [WeatherTool, YachtpitTools];
|
||||
|
||||
const callFunction = async (name, args) => {
|
||||
if (name === 'getCurrentTemperature') {
|
||||
return getCurrentTemp(args.location);
|
||||
if (name === 'get_weather') {
|
||||
return getWeather(args.latitude, args.longitude);
|
||||
}
|
||||
if (name === 'ship_control') {
|
||||
return yachtpitAi({ action: args.action, value: args.value });
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -19,38 +19,3 @@ export const BasicValueTool = {
|
||||
return { value: basic };
|
||||
},
|
||||
};
|
||||
export const WeatherTool = {
|
||||
name: 'get_weather',
|
||||
type: 'function',
|
||||
description: 'Get current temperature for a given location.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'City and country e.g. Bogotá, Colombia',
|
||||
},
|
||||
},
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
function: {
|
||||
name: 'getCurrentTemperature',
|
||||
description: 'Get the current temperature for a specific location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'The city and state, e.g., San Francisco, CA',
|
||||
},
|
||||
unit: {
|
||||
type: 'string',
|
||||
enum: ['Celsius', 'Fahrenheit'],
|
||||
description: "The temperature unit to use. Infer this from the user's location.",
|
||||
},
|
||||
},
|
||||
required: ['location', 'unit'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
25
packages/ai/src/tools/weather.ts
Normal file
25
packages/ai/src/tools/weather.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export async function getWeather(latitude: any, longitude: any) {
|
||||
const response = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m`,
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.current.temperature_2m;
|
||||
}
|
||||
|
||||
export const WeatherTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
description: 'Get current temperature for provided coordinates in celsius.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
latitude: { type: 'number' },
|
||||
longitude: { type: 'number' },
|
||||
},
|
||||
required: ['latitude', 'longitude'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true,
|
||||
},
|
||||
};
|
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.' };
|
||||
}
|
||||
}
|
@@ -43,6 +43,7 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"katex": "^0.16.20",
|
||||
"lucide-react": "^0.436.0",
|
||||
"mapbox-gl": "^3.13.0",
|
||||
"marked": "^15.0.4",
|
||||
"marked-extended-latex": "^1.1.0",
|
||||
"marked-footnote": "^1.2.4",
|
||||
@@ -54,6 +55,7 @@
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-map-gl": "^8.0.4",
|
||||
"react-streaming": "^0.4.2",
|
||||
"react-textarea-autosize": "^8.5.5",
|
||||
"react-use-pwa-install": "^1.0.3",
|
||||
|
@@ -32,6 +32,17 @@ 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...');
|
||||
@@ -49,15 +60,15 @@ function bundleCrate() {
|
||||
logger.info(`✅ Submodules already initialized at: ${yachtpitPath}`);
|
||||
}
|
||||
|
||||
// Build the yachtpit project
|
||||
const buildCwd = resolve(repoRoot, 'crates/yachtpit/crates/yachtpit');
|
||||
logger.info(`🔨 Building in directory: ${buildCwd}`);
|
||||
|
||||
try {
|
||||
execSync('trunk build --release', {
|
||||
cwd: buildCwd,
|
||||
});
|
||||
logger.info('✅ Yachtpit built');
|
||||
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);
|
||||
@@ -158,7 +169,6 @@ function bundleCrate() {
|
||||
|
||||
function optimizeWasmSize() {
|
||||
logger.info('🔨 Checking WASM size...');
|
||||
|
||||
const wasmPath = resolve(publicDir, 'yachtpit_bg.wasm');
|
||||
const fileSize = statSync(wasmPath).size;
|
||||
const sizeInMb = fileSize / (1024 * 1024);
|
||||
|
@@ -2,8 +2,7 @@ import { Box } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { BevyScene } from './BevyScene.tsx';
|
||||
import { MatrixRain } from './MatrixRain.tsx';
|
||||
import Particles from './Particles.tsx';
|
||||
import Map from './Map.tsx';
|
||||
import Tweakbox from './Tweakbox.tsx';
|
||||
|
||||
export const LandingComponent: React.FC = () => {
|
||||
@@ -13,6 +12,9 @@ export const LandingComponent: React.FC = () => {
|
||||
const [glow, setGlow] = useState(false);
|
||||
const [matrixRain, setMatrixRain] = useState(false);
|
||||
const [bevyScene, setBevyScene] = useState(true);
|
||||
const [mapActive, setMapActive] = useState(false);
|
||||
|
||||
const map = <Map visible={mapActive} />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -25,27 +27,18 @@ export const LandingComponent: React.FC = () => {
|
||||
>
|
||||
<Box
|
||||
position="fixed"
|
||||
bottom="24px"
|
||||
right="24px"
|
||||
bottom="100x"
|
||||
right="12px"
|
||||
maxWidth="300px"
|
||||
minWidth="200px"
|
||||
zIndex={1000}
|
||||
>
|
||||
<Tweakbox
|
||||
sliders={{
|
||||
speed: {
|
||||
value: !particles ? speed : 0.99,
|
||||
onChange: setSpeed,
|
||||
label: 'Animation Speed',
|
||||
min: 0.01,
|
||||
max: 0.99,
|
||||
step: 0.01,
|
||||
ariaLabel: 'animation-speed',
|
||||
},
|
||||
intensity: {
|
||||
value: !particles ? intensity : 0.99,
|
||||
onChange: setIntensity,
|
||||
label: 'Effect Intensity',
|
||||
label: 'Brightness',
|
||||
min: 0.01,
|
||||
max: 0.99,
|
||||
step: 0.01,
|
||||
@@ -53,50 +46,35 @@ export const LandingComponent: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
switches={{
|
||||
particles: {
|
||||
value: particles,
|
||||
onChange(enabled) {
|
||||
if (enabled) {
|
||||
setMatrixRain(!enabled);
|
||||
setBevyScene(!enabled);
|
||||
}
|
||||
setParticles(enabled);
|
||||
},
|
||||
label: 'Particles',
|
||||
},
|
||||
matrixRain: {
|
||||
value: matrixRain,
|
||||
onChange(enabled) {
|
||||
if (enabled) {
|
||||
setParticles(!enabled);
|
||||
setBevyScene(!enabled);
|
||||
}
|
||||
setMatrixRain(enabled);
|
||||
},
|
||||
label: 'Matrix Rain',
|
||||
},
|
||||
bevyScene: {
|
||||
value: bevyScene,
|
||||
onChange(enabled) {
|
||||
if (enabled) {
|
||||
setParticles(!enabled);
|
||||
setMatrixRain(!enabled);
|
||||
setMapActive(!enabled);
|
||||
}
|
||||
setBevyScene(enabled);
|
||||
},
|
||||
label: 'Bevy Scene',
|
||||
label: 'Instruments',
|
||||
},
|
||||
glow: {
|
||||
value: glow,
|
||||
onChange: setGlow,
|
||||
label: 'Glow Effect',
|
||||
GpsMap: {
|
||||
value: mapActive,
|
||||
onChange(enabled) {
|
||||
if (enabled) {
|
||||
setParticles(!enabled);
|
||||
setMatrixRain(!enabled);
|
||||
setBevyScene(!enabled);
|
||||
}
|
||||
setMapActive(enabled);
|
||||
},
|
||||
label: 'Map',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<BevyScene speed={speed} intensity={intensity} glow={glow} visible={bevyScene} />
|
||||
<MatrixRain speed={speed} intensity={intensity} glow={glow} visible={matrixRain} />
|
||||
<Particles glow speed={speed} intensity={intensity} visible={particles} />
|
||||
{mapActive && map}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
98
packages/client/src/components/landing-component/Map.tsx
Normal file
98
packages/client/src/components/landing-component/Map.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import ReactMap from 'react-map-gl/mapbox'; // ↔ v5+ uses this import path
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { Box, HStack, Button, Input, Center } from '@chakra-ui/react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
// Types for bevy_flurx_ipc communication
|
||||
interface GpsPosition {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
interface VesselStatus {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
heading: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface MapViewParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
interface AuthParams {
|
||||
authenticated: boolean;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
// public key
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
function Map(props: { visible: boolean }) {
|
||||
const [mapboxToken, setMapboxToken] = useState(atob(key));
|
||||
const [isTokenLoading, setIsTokenLoading] = useState(false);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setAuthenticated(true);
|
||||
setIsTokenLoading(false);
|
||||
}, []);
|
||||
|
||||
const [mapView, setMapView] = useState({
|
||||
longitude: -122.4,
|
||||
latitude: 37.8,
|
||||
zoom: 14,
|
||||
});
|
||||
|
||||
const handleNavigationClick = useCallback(async () => {
|
||||
console.log('handling navigation in map');
|
||||
}, []);
|
||||
|
||||
const handleSearchClick = useCallback(async () => {
|
||||
console.log('handling click search in map');
|
||||
}, []);
|
||||
|
||||
const handleMapViewChange = useCallback(async (evt: any) => {
|
||||
const { longitude, latitude, zoom } = evt.viewState;
|
||||
setMapView({ longitude, latitude, zoom });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={4}
|
||||
height="80%"
|
||||
width="100%"
|
||||
position="relative"
|
||||
display={props.visible ? undefined : 'none'}
|
||||
>
|
||||
<Box width={'100%'} height={'100%'} position="relative" zIndex={0}>
|
||||
{/* Map itself */}
|
||||
{authenticated && (
|
||||
<ReactMap
|
||||
mapboxAccessToken={mapboxToken}
|
||||
initialViewState={mapView}
|
||||
onMove={handleMapViewChange}
|
||||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||||
attributionControl={false}
|
||||
style={{ width: '100%', height: '100%' }} // let the wrapper dictate size
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||
<HStack position="relative" top={1} right={4} zIndex={1} justify={'right'}>
|
||||
<Button size="sm" variant="solid" onClick={handleNavigationClick}>
|
||||
Navigation
|
||||
</Button>
|
||||
<Button size="sm" variant="solid" onClick={handleSearchClick}>
|
||||
Search
|
||||
</Button>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Map;
|
@@ -10,7 +10,7 @@ export default function Hero() {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Box p={2} mt={2}>
|
||||
<Box>
|
||||
<Heading
|
||||
textAlign={isMobile ? 'left' : 'right'}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Stack } from '@chakra-ui/react';
|
||||
import { Grid, GridItem, Stack } from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import Chat from '../../components/chat/Chat.tsx';
|
||||
import { LandingComponent } from '../../components/landing-component/LandingComponent.tsx';
|
||||
import clientChatStore from '../../stores/ClientChatStore';
|
||||
|
||||
@@ -15,11 +16,14 @@ export default function IndexPage() {
|
||||
// Fall back to default model
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack direction="column" height="100%" width="100%" spacing={0}>
|
||||
<LandingComponent />
|
||||
{/*<Chat height="100%" width="100%" />*/}
|
||||
</Stack>
|
||||
<Grid templateColumns="repeat(2, 1fr)" height="100%" width="100%" gap={0}>
|
||||
<GridItem>
|
||||
<LandingComponent />
|
||||
</GridItem>
|
||||
<GridItem p={2}>
|
||||
<Chat />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export const welcome_home_text = `
|
||||
# welcome!
|
||||
# yachtpit-ai
|
||||
---
|
||||
Please enjoy [responsibly](https://centerforresponsible.ai/the-center)
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
`;
|
||||
|
Reference in New Issue
Block a user