chat + maps + ai + tools

This commit is contained in:
geoffsee
2025-07-08 13:53:36 -04:00
committed by Geoff Seemueller
parent 48655474e3
commit 818e0e672a
14 changed files with 384 additions and 105 deletions

View File

@@ -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 });
}
};

View File

@@ -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'],
},
},
};

View 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}&current=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,
},
};

View 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.' };
}
}

View File

@@ -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",

View File

@@ -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);

View File

@@ -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>
);
};

View 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;

View File

@@ -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'}

View File

@@ -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>
);
}

View File

@@ -1,7 +1,7 @@
export const welcome_home_text = `
# welcome!
# yachtpit-ai
---
Please enjoy [responsibly](https://centerforresponsible.ai/the-center)
<br/>
<br/>
`;