mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
Compare commits
13 Commits
dependabot
...
smart-land
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7ab1141540 | ||
![]() |
5630a95f1a | ||
![]() |
24351b0be7 | ||
![]() |
cd58a23942 | ||
![]() |
fbd696612a | ||
![]() |
b737ff09b3 | ||
![]() |
c436ae1b62 | ||
![]() |
5f6cb3d6c7 | ||
![]() |
195d071c3c | ||
![]() |
a996f115bc | ||
![]() |
3bbd4243c5 | ||
![]() |
944b956ffd | ||
![]() |
c3ea9ba599 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "crates/yachtpit"]
|
||||||
|
path = crates/yachtpit
|
||||||
|
url = https://github.com/seemueller-io/yachtpit.git
|
1
crates/yachtpit
Submodule
1
crates/yachtpit
Submodule
Submodule crates/yachtpit added at 348f20641c
@@ -10,6 +10,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "packages/scripts/cleanup.sh",
|
"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",
|
"test:all": "bun run --filter='*' tests",
|
||||||
"client:dev": "(cd packages/client && bun run dev)",
|
"client:dev": "(cd packages/client && bun run dev)",
|
||||||
"server:dev": "bun build:client && (cd packages/server && bun run dev)",
|
"server:dev": "bun build:client && (cd packages/server && bun run dev)",
|
||||||
|
@@ -46,6 +46,7 @@ describe('AssistantSdk', () => {
|
|||||||
|
|
||||||
expect(prompt).toContain('# Assistant Knowledge');
|
expect(prompt).toContain('# Assistant Knowledge');
|
||||||
expect(prompt).toContain('### Date: ');
|
expect(prompt).toContain('### Date: ');
|
||||||
|
expect(prompt).toContain('### Web Host: ');
|
||||||
expect(prompt).toContain('### User Location: ');
|
expect(prompt).toContain('### User Location: ');
|
||||||
expect(prompt).toContain('### Timezone: ');
|
expect(prompt).toContain('### Timezone: ');
|
||||||
});
|
});
|
||||||
|
@@ -23,7 +23,7 @@ export class AssistantSdk {
|
|||||||
|
|
||||||
return `# Assistant Knowledge
|
return `# Assistant Knowledge
|
||||||
## Assistant Name
|
## Assistant Name
|
||||||
### open-gsio
|
### yachtpit-ai
|
||||||
## Current Context
|
## Current Context
|
||||||
### Date: ${currentDate} ${currentTime}
|
### Date: ${currentDate} ${currentTime}
|
||||||
${maxTokens ? `### Max Response Length: ${maxTokens} tokens (maximum)` : ''}
|
${maxTokens ? `### Max Response Length: ${maxTokens} tokens (maximum)` : ''}
|
||||||
|
@@ -15,21 +15,10 @@ export class FireworksAiChatProvider extends BaseChatProvider {
|
|||||||
let modelPrefix = 'accounts/fireworks/models/';
|
let modelPrefix = 'accounts/fireworks/models/';
|
||||||
if (param.model.toLowerCase().includes('yi-')) {
|
if (param.model.toLowerCase().includes('yi-')) {
|
||||||
modelPrefix = 'accounts/yi-01-ai/models/';
|
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 {
|
return {
|
||||||
model: finalModelIdentifier,
|
model: `${modelPrefix}${param.model}`,
|
||||||
messages: safeMessages,
|
messages: safeMessages,
|
||||||
stream: true,
|
stream: true,
|
||||||
};
|
};
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
"generate:sitemap": "bun ./scripts/generate_sitemap.js open-gsio.seemueller.workers.dev",
|
"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:robotstxt": "bun ./scripts/generate_robots_txt.js open-gsio.seemueller.workers.dev",
|
||||||
"generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static",
|
"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'"
|
"generate:pwa:assets": "test ! -f public/pwa-64x64.png && pwa-assets-generator --preset minimal-2023 public/logo.png || echo 'PWA assets already exist'"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chakra-ui/icons": "^2.2.4",
|
"@chakra-ui/icons": "^2.2.4",
|
||||||
"@chakra-ui/react": "^3.24.2",
|
"@chakra-ui/react": "^2.10.6",
|
||||||
"@cloudflare/workers-types": "^4.20241205.0",
|
"@cloudflare/workers-types": "^4.20241205.0",
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.13.5",
|
||||||
"@emotion/styled": "^11.13.5",
|
"@emotion/styled": "^11.13.5",
|
||||||
|
196
packages/client/scripts/generate-bevy-bundle.js
Normal file
196
packages/client/scripts/generate-bevy-bundle.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { execSync } 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...`);
|
||||||
|
execSync(`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();
|
35
packages/client/src/components/InstallButton.tsx
Normal file
35
packages/client/src/components/InstallButton.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { IconButton } from '@chakra-ui/react';
|
||||||
|
import { HardDriveDownload } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { toolbarButtonZIndex } from './toolbar/Toolbar.tsx';
|
||||||
|
|
||||||
|
function InstallButton() {
|
||||||
|
// const install = usePWAInstall();
|
||||||
|
|
||||||
|
const install = () => {
|
||||||
|
console.warn('this does not work in all browsers');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Install App"
|
||||||
|
title="Install App"
|
||||||
|
icon={<HardDriveDownload />}
|
||||||
|
size="md"
|
||||||
|
bg="transparent"
|
||||||
|
stroke="text.accent"
|
||||||
|
color="text.accent"
|
||||||
|
onClick={() => install}
|
||||||
|
_hover={{
|
||||||
|
bg: 'transparent',
|
||||||
|
svg: {
|
||||||
|
stroke: 'accent.secondary',
|
||||||
|
transition: 'stroke 0.3s ease-in-out',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
zIndex={toolbarButtonZIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstallButton;
|
@@ -171,7 +171,7 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
|||||||
bg="background.tertiary"
|
bg="background.tertiary"
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clientChatStore.reset();
|
clientChatStore.setActiveConversation('conversation:new');
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
|
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
|
||||||
|
@@ -49,7 +49,7 @@ const InputTextArea: React.FC<InputTextAreaProps> = observer(
|
|||||||
color="text.primary"
|
color="text.primary"
|
||||||
borderRadius="20px"
|
borderRadius="20px"
|
||||||
border="none"
|
border="none"
|
||||||
placeholder="Free my mind..."
|
placeholder="To Gilligan's island!"
|
||||||
_placeholder={{
|
_placeholder={{
|
||||||
color: 'gray.400',
|
color: 'gray.400',
|
||||||
textWrap: 'nowrap',
|
textWrap: 'nowrap',
|
||||||
|
@@ -9,7 +9,7 @@ export function formatConversationMarkdown(messages: Instance<typeof IMessage>[]
|
|||||||
if (message.role === 'user') {
|
if (message.role === 'user') {
|
||||||
return `**You**: ${message.content}`;
|
return `**You**: ${message.content}`;
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
return `**open-gsio**: ${message.content}`;
|
return `**yachtpit-ai**: ${message.content}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
|
@@ -51,7 +51,7 @@ const MessageBubble = observer(({ msg, scrollRef }) => {
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isUser = msg.role === 'user';
|
const isUser = msg.role === 'user';
|
||||||
const senderName = isUser ? 'You' : 'open-gsio';
|
const senderName = isUser ? 'You' : 'yachtpit-ai';
|
||||||
const isLoading = !msg.content || !(msg.content.trim().length > 0);
|
const isLoading = !msg.content || !(msg.content.trim().length > 0);
|
||||||
const messageRef = useRef();
|
const messageRef = useRef();
|
||||||
|
|
||||||
|
@@ -104,7 +104,7 @@ describe('MessageBubble', () => {
|
|||||||
it('should render assistant message correctly', () => {
|
it('should render assistant message correctly', () => {
|
||||||
render(<MessageBubble msg={mockAssistantMessage} scrollRef={mockScrollRef} />);
|
render(<MessageBubble msg={mockAssistantMessage} scrollRef={mockScrollRef} />);
|
||||||
|
|
||||||
expect(screen.getByText('open-gsio')).toBeInTheDocument();
|
expect(screen.getByText('yachtpit-ai')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('message-content')).toHaveTextContent('Assistant response');
|
expect(screen.getByTestId('message-content')).toHaveTextContent('Assistant response');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
function InstallButton() {
|
|
||||||
return <button onClick={handleInstall}>Install App</button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InstallButton;
|
|
@@ -1,61 +0,0 @@
|
|||||||
import { IconButton } from '@chakra-ui/react';
|
|
||||||
import { HardDriveDownload } from 'lucide-react';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { toolbarButtonZIndex } from '../toolbar/Toolbar.tsx';
|
|
||||||
|
|
||||||
function InstallButton() {
|
|
||||||
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
|
||||||
const [isInstalled, setIsInstalled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleBeforeInstallPrompt = e => {
|
|
||||||
// Prevent the default prompt
|
|
||||||
e.preventDefault();
|
|
||||||
setDeferredPrompt(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleInstall = () => {
|
|
||||||
if (deferredPrompt) {
|
|
||||||
deferredPrompt.prompt();
|
|
||||||
deferredPrompt.userChoice.then(choiceResult => {
|
|
||||||
if (choiceResult.outcome === 'accepted') {
|
|
||||||
console.log('User accepted the installation prompt');
|
|
||||||
} else {
|
|
||||||
console.log('User dismissed the installation prompt');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setDeferredPrompt(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
aria-label="Install App"
|
|
||||||
title="Install App"
|
|
||||||
icon={<HardDriveDownload />}
|
|
||||||
size="md"
|
|
||||||
bg="transparent"
|
|
||||||
stroke="text.accent"
|
|
||||||
color="text.accent"
|
|
||||||
onClick={handleInstall}
|
|
||||||
_hover={{
|
|
||||||
bg: 'transparent',
|
|
||||||
svg: {
|
|
||||||
stroke: 'accent.secondary',
|
|
||||||
transition: 'stroke 0.3s ease-in-out',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
zIndex={toolbarButtonZIndex}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InstallButton;
|
|
@@ -0,0 +1,58 @@
|
|||||||
|
import { Box, useBreakpointValue } from '@chakra-ui/react';
|
||||||
|
import React, { memo, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
export interface BevySceneProps {
|
||||||
|
speed?: number;
|
||||||
|
intensity?: number; // 0-1 when visible
|
||||||
|
glow?: boolean;
|
||||||
|
visible?: boolean; // NEW — defaults to true
|
||||||
|
}
|
||||||
|
|
||||||
|
const BevySceneInner: React.FC<BevySceneProps> = ({
|
||||||
|
speed = 1,
|
||||||
|
intensity = 1,
|
||||||
|
glow = false,
|
||||||
|
visible,
|
||||||
|
}) => {
|
||||||
|
const maxWidth = useBreakpointValue({ base: 640, md: 720 }, { ssr: true });
|
||||||
|
|
||||||
|
/* initialise once */
|
||||||
|
useEffect(() => {
|
||||||
|
let dispose: (() => void) | void;
|
||||||
|
(async () => {
|
||||||
|
const { default: init } = await import(/* webpackIgnore: true */ '/public/yachtpit.js');
|
||||||
|
dispose = await init(); // zero-arg, uses #yachtpit-canvas
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
if (typeof dispose === 'function') dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/* memoised styles */
|
||||||
|
const wrapperStyles = useMemo(
|
||||||
|
() => ({
|
||||||
|
position: 'absolute' as const,
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
opacity: visible ? Math.min(Math.max(intensity, 0), 1) : 0,
|
||||||
|
filter: glow ? 'blur(1px)' : 'none',
|
||||||
|
transition: `opacity ${speed}s ease-in-out`,
|
||||||
|
display: visible ? 'block' : 'none', // optional: reclaim hit-testing entirely
|
||||||
|
}),
|
||||||
|
[visible, intensity, glow, speed],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box as="div" sx={wrapperStyles}>
|
||||||
|
<canvas
|
||||||
|
id="yachtpit-canvas"
|
||||||
|
width={useBreakpointValue({ base: 640, md: 1280 }, { ssr: true })}
|
||||||
|
height={useBreakpointValue({ base: 360, md: 720 }, { ssr: true })}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BevyScene = memo(BevySceneInner);
|
@@ -3,11 +3,14 @@ import React, { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { useComponent } from '../contexts/ComponentContext.tsx';
|
import { useComponent } from '../contexts/ComponentContext.tsx';
|
||||||
|
|
||||||
// import { BevyScene } from './BevyScene.tsx';
|
import { BevyScene } from './BevyScene.tsx';
|
||||||
import Tweakbox from './Tweakbox.tsx';
|
import Tweakbox from './Tweakbox.tsx';
|
||||||
|
|
||||||
export const LandingComponent: React.FC = () => {
|
export const LandingComponent: React.FC = () => {
|
||||||
|
const [speed, setSpeed] = useState(0.2);
|
||||||
const [intensity, setIntensity] = useState(0.99);
|
const [intensity, setIntensity] = useState(0.99);
|
||||||
|
const [glow, setGlow] = useState(false);
|
||||||
|
const [bevyScene, setBevyScene] = useState(true);
|
||||||
const [mapActive, setMapActive] = useState(true);
|
const [mapActive, setMapActive] = useState(true);
|
||||||
const [aiActive, setAiActive] = useState(false);
|
const [aiActive, setAiActive] = useState(false);
|
||||||
|
|
||||||
@@ -24,8 +27,22 @@ export const LandingComponent: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="section" bg="background.primary" overflow="hidden">
|
<Box
|
||||||
<Box position="fixed" right={0} maxWidth="300px" minWidth="200px" zIndex={1000}>
|
as="section"
|
||||||
|
bg="background.primary"
|
||||||
|
w="100%"
|
||||||
|
h="100vh"
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="fixed"
|
||||||
|
bottom="100x"
|
||||||
|
right="12px"
|
||||||
|
maxWidth="300px"
|
||||||
|
minWidth="200px"
|
||||||
|
zIndex={1000}
|
||||||
|
>
|
||||||
<Tweakbox
|
<Tweakbox
|
||||||
sliders={{
|
sliders={{
|
||||||
intensity: {
|
intensity: {
|
||||||
@@ -39,6 +56,13 @@ export const LandingComponent: React.FC = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
switches={{
|
switches={{
|
||||||
|
bevyScene: {
|
||||||
|
value: bevyScene,
|
||||||
|
onChange(enabled) {
|
||||||
|
setBevyScene(enabled);
|
||||||
|
},
|
||||||
|
label: 'Instruments',
|
||||||
|
},
|
||||||
GpsMap: {
|
GpsMap: {
|
||||||
value: mapActive,
|
value: mapActive,
|
||||||
onChange(enabled) {
|
onChange(enabled) {
|
||||||
@@ -50,7 +74,7 @@ export const LandingComponent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
setMapActive(enabled);
|
setMapActive(enabled);
|
||||||
},
|
},
|
||||||
label: 'GPS',
|
label: 'Map',
|
||||||
},
|
},
|
||||||
AI: {
|
AI: {
|
||||||
value: aiActive,
|
value: aiActive,
|
||||||
@@ -68,7 +92,7 @@ export const LandingComponent: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/*<BevyScene speed={speed} intensity={intensity} glow={glow} visible={bevyScene} />*/}
|
<BevyScene speed={speed} intensity={intensity} glow={glow} visible={bevyScene} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -37,7 +37,7 @@ const key =
|
|||||||
function Map(props: { visible: boolean }) {
|
function Map(props: { visible: boolean }) {
|
||||||
return (
|
return (
|
||||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||||
<Box position={'absolute'} top={0} w="100vw" h={'100vh'} overflow="hidden">
|
<Box w="100%" h="100vh" position="relative" overflow="hidden">
|
||||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||||
|
|
||||||
<MapNext mapboxPublicKey={atob(key)} />
|
<MapNext mapboxPublicKey={atob(key)} />
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, Button, HStack, Input } from '@chakra-ui/react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Map, {
|
import Map, {
|
||||||
FullscreenControl,
|
FullscreenControl,
|
||||||
@@ -118,10 +118,11 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
|||||||
right: 0,
|
right: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GeolocateControl position="top-left" style={{ marginTop: '6rem' }} />
|
<GeolocateControl position="top-left" />
|
||||||
<FullscreenControl position="top-left" />
|
<FullscreenControl position="top-left" />
|
||||||
<NavigationControl position="top-left" />
|
<NavigationControl position="top-left" />
|
||||||
<ScaleControl position="top-left" />
|
<ScaleControl position="top-left" />
|
||||||
|
|
||||||
{pins}
|
{pins}
|
||||||
|
|
||||||
{popupInfo && (
|
{popupInfo && (
|
||||||
|
@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import BuiltWithButton from '../BuiltWithButton';
|
import BuiltWithButton from '../BuiltWithButton';
|
||||||
import InstallButton from '../install/InstallButton.tsx';
|
import InstallButton from '../InstallButton.tsx';
|
||||||
|
|
||||||
import GithubButton from './GithubButton';
|
import GithubButton from './GithubButton';
|
||||||
import SupportThisSiteButton from './SupportThisSiteButton';
|
import SupportThisSiteButton from './SupportThisSiteButton';
|
||||||
|
@@ -6,7 +6,7 @@ import { useIsMobile } from '../components/contexts/MobileContext';
|
|||||||
function Content({ children }) {
|
function Content({ children }) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" w="100%" h="100vh">
|
<Flex flexDirection="column" w="100%" h="100vh" p={!isMobile ? 4 : 1}>
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
// runs before anything else
|
// runs before anything else
|
||||||
import { registerSW } from 'virtual:pwa-register';
|
|
||||||
|
|
||||||
import UserOptionsStore from '../stores/UserOptionsStore';
|
import UserOptionsStore from '../stores/UserOptionsStore';
|
||||||
|
|
||||||
UserOptionsStore.initialize();
|
UserOptionsStore.initialize();
|
||||||
@@ -8,11 +6,7 @@ UserOptionsStore.initialize();
|
|||||||
try {
|
try {
|
||||||
const isLocal = window.location.hostname.includes('localhost');
|
const isLocal = window.location.hostname.includes('localhost');
|
||||||
if (!isLocal) {
|
if (!isLocal) {
|
||||||
if ('serviceWorker' in navigator) {
|
navigator.serviceWorker.register('/service-worker.js');
|
||||||
// && !/localhost/.test(window.location)) {
|
|
||||||
registerSW();
|
|
||||||
}
|
|
||||||
// navigator.serviceWorker.register('/service-worker.js');
|
|
||||||
} else {
|
} else {
|
||||||
(async () => {
|
(async () => {
|
||||||
await navigator.serviceWorker.getRegistrations().then(registrations => {
|
await navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, Grid, GridItem, Stack } from '@chakra-ui/react';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import Chat from '../../components/chat/Chat.tsx';
|
import Chat from '../../components/chat/Chat.tsx';
|
||||||
@@ -22,26 +22,27 @@ export default function IndexPage() {
|
|||||||
const component = useComponent();
|
const component = useComponent();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box height="100%" width="100%">
|
<Grid templateColumns="repeat(2, 1fr)" height="100%" width="100%" gap={0}>
|
||||||
<LandingComponent />
|
<GridItem>
|
||||||
|
<LandingComponent />
|
||||||
<Box
|
</GridItem>
|
||||||
display={component.enabledComponent === 'ai' ? undefined : 'none'}
|
<GridItem p={2}>
|
||||||
width="100%"
|
<Box
|
||||||
height="100%"
|
display={component.enabledComponent === 'ai' ? undefined : 'none'}
|
||||||
overflowY="scroll"
|
width="100%"
|
||||||
padding={'unset'}
|
height="100%"
|
||||||
>
|
overflowY="scroll"
|
||||||
<Chat />
|
>
|
||||||
</Box>
|
<Chat />
|
||||||
<Box
|
</Box>
|
||||||
display={component.enabledComponent === 'gpsmap' ? undefined : 'none'}
|
<Box
|
||||||
width="100%"
|
display={component.enabledComponent === 'gpsmap' ? undefined : 'none'}
|
||||||
height="100%"
|
width="100%"
|
||||||
padding={'unset'}
|
height="100%"
|
||||||
>
|
>
|
||||||
<ReactMap visible={component.enabledComponent === 'gpsmap'} />
|
<ReactMap visible={component.enabledComponent === 'gpsmap'} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'/': { sidebarLabel: 'Home', heroLabel: 'o-gsio' },
|
'/': { sidebarLabel: 'Home', heroLabel: 'gsio' },
|
||||||
'/connect': { sidebarLabel: 'Connect', heroLabel: 'connect' },
|
'/connect': { sidebarLabel: 'Connect', heroLabel: 'connect' },
|
||||||
'/privacy-policy': {
|
'/privacy-policy': {
|
||||||
sidebarLabel: '',
|
sidebarLabel: '',
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export const welcome_home_text = `
|
export const welcome_home_text = `
|
||||||
# open-gsio
|
# yachtpit-ai
|
||||||
---
|
---
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
@@ -22,6 +22,10 @@ const prebuildPlugin = () => ({
|
|||||||
console.log('Generated robots.txt -> public/robots.txt');
|
console.log('Generated robots.txt -> public/robots.txt');
|
||||||
child_process.execSync('bun run generate:fonts');
|
child_process.execSync('bun run generate:fonts');
|
||||||
console.log('Copied fonts -> public/static/fonts');
|
console.log('Copied fonts -> public/static/fonts');
|
||||||
|
child_process.execSync('bun run generate:bevy:bundle', {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
console.log('Bundled bevy app -> public/yachtpit.html');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -161,29 +161,19 @@ const ChatService = types
|
|||||||
|
|
||||||
const openai = new OpenAI({ apiKey: provider.key, baseURL: provider.endpoint });
|
const openai = new OpenAI({ apiKey: provider.key, baseURL: provider.endpoint });
|
||||||
|
|
||||||
const basicFilters = (model: any) => {
|
// 2‑a. List models
|
||||||
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 {
|
try {
|
||||||
const listResp: any = yield openai.models.list(); // <‑‑ async
|
const listResp: any = yield openai.models.list(); // <‑‑ async
|
||||||
const models = 'data' in listResp ? listResp.data : listResp;
|
const models = 'data' in listResp ? listResp.data : listResp;
|
||||||
|
|
||||||
providerModels.set(
|
providerModels.set(
|
||||||
provider.name,
|
provider.name,
|
||||||
models.filter((mdl: any) => {
|
models.filter(
|
||||||
if ('supports_chat' in mdl && mdl.supports_chat) {
|
(mdl: any) =>
|
||||||
return basicFilters(mdl);
|
!mdl.id.includes('whisper') &&
|
||||||
} else if ('supports_chat' in mdl && !mdl.supports_chat) {
|
!mdl.id.includes('tts') &&
|
||||||
return false;
|
!mdl.id.includes('guard'),
|
||||||
}
|
),
|
||||||
return basicFilters(mdl);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2‑b. Retrieve metadata
|
// 2‑b. Retrieve metadata
|
||||||
@@ -328,8 +318,7 @@ const ChatService = types
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (message.includes('404')) {
|
if (message.includes('404')) {
|
||||||
console.log(message);
|
throw new ClientError(`Something went wrong, try again.`, 413, {});
|
||||||
throw new ClientError(`Something went wrong, try again.`, 404, {});
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user