mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
- Introduce MapStore
to manage map state and controls using MobX-State-Tree.
- Integrate `MapStore` into `ClientChatStore`. - Add support for handling map control tool responses in `StreamStore`. - Update `InputMenu` with loading state while fetching models and UI improvements. - Include `useLayoutEffect` in `LandingComponent` for persistent state management. - Enhance `ChatService` with debug logs, model fallback handling, and better error reporting.
This commit is contained in:
@@ -24,10 +24,68 @@ export class ProviderRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static async getModelFamily(model: any, env: GenericEnv) {
|
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 allModels = await env.KV_STORAGE.get('supportedModels');
|
||||||
const models = JSON.parse(allModels);
|
const models = JSON.parse(allModels);
|
||||||
const modelData = models.filter((m: ModelMeta) => m.id === model);
|
|
||||||
return modelData[0].provider;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getModelMeta(meta: any, env: GenericEnv) {
|
static async getModelMeta(meta: any, env: GenericEnv) {
|
||||||
@@ -41,12 +99,19 @@ export class ProviderRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProviders(env: GenericEnv) {
|
setProviders(env: GenericEnv) {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Starting provider detection');
|
||||||
|
|
||||||
const indicies = {
|
const indicies = {
|
||||||
providerName: 0,
|
providerName: 0,
|
||||||
providerValue: 1,
|
providerValue: 1,
|
||||||
};
|
};
|
||||||
const valueDelimiter = '_';
|
const valueDelimiter = '_';
|
||||||
const envKeys = Object.keys(env);
|
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++) {
|
for (let i = 0; i < envKeys.length; i++) {
|
||||||
if (envKeys.at(i)?.endsWith('KEY')) {
|
if (envKeys.at(i)?.endsWith('KEY')) {
|
||||||
const detectedProvider = envKeys
|
const detectedProvider = envKeys
|
||||||
@@ -55,9 +120,15 @@ export class ProviderRepository {
|
|||||||
.at(indicies.providerName)
|
.at(indicies.providerName)
|
||||||
?.toLowerCase();
|
?.toLowerCase();
|
||||||
const detectedProviderValue = env[envKeys.at(i) as string];
|
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) {
|
if (detectedProviderValue) {
|
||||||
switch (detectedProvider) {
|
switch (detectedProvider) {
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Claude provider (anthropic)');
|
||||||
this.#providers.push({
|
this.#providers.push({
|
||||||
name: 'claude',
|
name: 'claude',
|
||||||
key: env.ANTHROPIC_API_KEY,
|
key: env.ANTHROPIC_API_KEY,
|
||||||
@@ -65,6 +136,8 @@ export class ProviderRepository {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Google provider (gemini)');
|
||||||
this.#providers.push({
|
this.#providers.push({
|
||||||
name: 'google',
|
name: 'google',
|
||||||
key: env.GEMINI_API_KEY,
|
key: env.GEMINI_API_KEY,
|
||||||
@@ -72,6 +145,8 @@ export class ProviderRepository {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'cloudflare':
|
case 'cloudflare':
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Cloudflare provider');
|
||||||
this.#providers.push({
|
this.#providers.push({
|
||||||
name: 'cloudflare',
|
name: 'cloudflare',
|
||||||
key: env.CLOUDFLARE_API_KEY,
|
key: env.CLOUDFLARE_API_KEY,
|
||||||
@@ -82,6 +157,8 @@ export class ProviderRepository {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Adding default provider "${detectedProvider}"`);
|
||||||
this.#providers.push({
|
this.#providers.push({
|
||||||
name: detectedProvider as SupportedProvider,
|
name: detectedProvider as SupportedProvider,
|
||||||
key: env[envKeys[i] as string],
|
key: env[envKeys[i] as string],
|
||||||
@@ -89,8 +166,14 @@ export class ProviderRepository {
|
|||||||
ProviderRepository.OPENAI_COMPAT_ENDPOINTS[detectedProvider as SupportedProvider],
|
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 })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -236,7 +236,7 @@ export abstract class BaseChatProvider implements ChatStreamProvider {
|
|||||||
|
|
||||||
// Process chunk normally for non-tool-call responses
|
// Process chunk normally for non-tool-call responses
|
||||||
if (!chunk.choices[0]?.delta?.tool_calls) {
|
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);
|
const shouldBreak = await this.processChunk(chunk, dataCallback);
|
||||||
if (shouldBreak) {
|
if (shouldBreak) {
|
||||||
conversationComplete = true;
|
conversationComplete = true;
|
||||||
|
@@ -18,7 +18,7 @@ export class FireworksAiChatProvider extends BaseChatProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model: `${modelPrefix}${param.model}`,
|
model: `${param.model}`,
|
||||||
messages: safeMessages,
|
messages: safeMessages,
|
||||||
stream: true,
|
stream: true,
|
||||||
};
|
};
|
||||||
|
@@ -9,14 +9,14 @@ export interface MapsControlResult {
|
|||||||
*/
|
*/
|
||||||
export const MapsTools = {
|
export const MapsTools = {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
description:
|
|
||||||
'Interface for controlling a web-rendered map to explore publicly available geospatial data',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock implementation of a maps control command.
|
* Mock implementation of a maps control command.
|
||||||
*/
|
*/
|
||||||
function: {
|
function: {
|
||||||
name: 'maps_control',
|
name: 'maps_control',
|
||||||
|
description:
|
||||||
|
'Interface for controlling a web-rendered map to explore publicly available geospatial data',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@@ -8,6 +8,7 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useOutsideClick,
|
useOutsideClick,
|
||||||
@@ -41,19 +42,38 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
|||||||
|
|
||||||
const [controlledOpen, setControlledOpen] = useState<boolean>(false);
|
const [controlledOpen, setControlledOpen] = useState<boolean>(false);
|
||||||
const [supportedModels, setSupportedModels] = useState<any[]>([]);
|
const [supportedModels, setSupportedModels] = useState<any[]>([]);
|
||||||
|
const [isLoadingModels, setIsLoadingModels] = useState<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setControlledOpen(isOpen);
|
setControlledOpen(isOpen);
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setIsLoadingModels(true);
|
||||||
fetch('/api/models')
|
fetch('/api/models')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(models => {
|
.then(models => {
|
||||||
setSupportedModels(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 => {
|
.catch(err => {
|
||||||
console.error('Could not fetch models: ', err);
|
console.error('Could not fetch models: ', err);
|
||||||
|
setIsLoadingModels(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -108,8 +128,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
|||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
bg="text.accent"
|
bg="text.accent"
|
||||||
icon={<Settings size={20} />}
|
icon={isLoadingModels ? <Spinner size="sm" /> : <Settings size={20} />}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled || isLoadingModels}
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
_hover={{ bg: 'rgba(255, 255, 255, 0.2)' }}
|
_hover={{ bg: 'rgba(255, 255, 255, 0.2)' }}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
@@ -118,8 +138,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
|||||||
) : (
|
) : (
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
rightIcon={<ChevronDown size={16} />}
|
rightIcon={isLoadingModels ? <Spinner size="sm" /> : <ChevronDown size={16} />}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled || isLoadingModels}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
@@ -128,7 +148,7 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled })
|
|||||||
{...MsM_commonButtonStyles}
|
{...MsM_commonButtonStyles}
|
||||||
>
|
>
|
||||||
<Text noOfLines={1} maxW="100px" fontSize="sm">
|
<Text noOfLines={1} maxW="100px" fontSize="sm">
|
||||||
{clientChatStore.model}
|
{isLoadingModels ? 'Loading...' : clientChatStore.model}
|
||||||
</Text>
|
</Text>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useComponent } from '../contexts/ComponentContext.tsx';
|
import { useComponent } from '../contexts/ComponentContext.tsx';
|
||||||
|
|
||||||
@@ -11,6 +11,23 @@ export const LandingComponent: React.FC = () => {
|
|||||||
const [mapActive, setMapActive] = useState(true);
|
const [mapActive, setMapActive] = useState(true);
|
||||||
const [aiActive, setAiActive] = useState(false);
|
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 component = useComponent();
|
||||||
const { setEnabledComponent } = component;
|
const { setEnabledComponent } = component;
|
||||||
|
|
||||||
@@ -21,12 +38,14 @@ export const LandingComponent: React.FC = () => {
|
|||||||
if (aiActive) {
|
if (aiActive) {
|
||||||
setEnabledComponent('ai');
|
setEnabledComponent('ai');
|
||||||
}
|
}
|
||||||
}, []);
|
}, [mapActive, aiActive, setEnabledComponent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="section" bg="background.primary" overflow="hidden">
|
<Box as="section" bg="background.primary" overflow="hidden">
|
||||||
<Box position="fixed" right={0} maxWidth="300px" minWidth="200px" zIndex={1000}>
|
<Box position="fixed" right={0} maxWidth="300px" minWidth="200px" zIndex={1000}>
|
||||||
<Tweakbox
|
<Tweakbox
|
||||||
|
id="app-tweaker"
|
||||||
|
persist={true}
|
||||||
sliders={{
|
sliders={{
|
||||||
intensity: {
|
intensity: {
|
||||||
value: intensity,
|
value: intensity,
|
||||||
@@ -68,7 +87,6 @@ export const LandingComponent: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/*<BevyScene speed={speed} intensity={intensity} glow={glow} visible={bevyScene} />*/}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -37,10 +37,10 @@ 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 position={'absolute'} top={0} w="100%" h={'100vh'} overflow="hidden">
|
||||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||||
|
|
||||||
<MapNext mapboxPublicKey={atob(key)} />
|
<MapNext mapboxPublicKey={atob(key)} visible={props.visible} />
|
||||||
{/*<Map*/}
|
{/*<Map*/}
|
||||||
{/* mapboxAccessToken={atob(key)}*/}
|
{/* mapboxAccessToken={atob(key)}*/}
|
||||||
{/* initialViewState={mapView}*/}
|
{/* initialViewState={mapView}*/}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import Map, {
|
import Map, {
|
||||||
FullscreenControl,
|
FullscreenControl,
|
||||||
GeolocateControl,
|
GeolocateControl,
|
||||||
@@ -9,25 +10,36 @@ import Map, {
|
|||||||
ScaleControl,
|
ScaleControl,
|
||||||
} from 'react-map-gl/mapbox';
|
} from 'react-map-gl/mapbox';
|
||||||
|
|
||||||
|
import clientChatStore from '../../stores/ClientChatStore';
|
||||||
|
|
||||||
import PORTS from './nautical-base-data.json';
|
import PORTS from './nautical-base-data.json';
|
||||||
import Pin from './pin';
|
import Pin from './pin';
|
||||||
|
|
||||||
export default function MapNext(props: any = { mapboxPublicKey: '' } as any) {
|
function MapNextComponent(props: any = { mapboxPublicKey: '', visible: true } as any) {
|
||||||
const [popupInfo, setPopupInfo] = useState(null);
|
const [popupInfo, setPopupInfo] = useState(null);
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [isTokenLoading, setIsTokenLoading] = useState(false);
|
const [isTokenLoading, setIsTokenLoading] = useState(false);
|
||||||
const [authenticated, setAuthenticated] = useState(false);
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
|
const mapRef = useRef<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
setIsTokenLoading(false);
|
setIsTokenLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [mapView, setMapView] = useState({
|
// Handle map resize when component becomes visible
|
||||||
longitude: -122.4,
|
useEffect(() => {
|
||||||
latitude: 37.8,
|
if (props.visible && mapRef.current) {
|
||||||
zoom: 14,
|
// 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 handleNavigationClick = useCallback(async () => {
|
const handleNavigationClick = useCallback(async () => {
|
||||||
console.log('handling navigation in map');
|
console.log('handling navigation in map');
|
||||||
@@ -39,7 +51,10 @@ export default function MapNext(props: any = { mapboxPublicKey: '' } as any) {
|
|||||||
|
|
||||||
const handleMapViewChange = useCallback(async (evt: any) => {
|
const handleMapViewChange = useCallback(async (evt: any) => {
|
||||||
const { longitude, latitude, zoom } = evt.viewState;
|
const { longitude, latitude, zoom } = evt.viewState;
|
||||||
setMapView({ longitude, latitude, zoom });
|
clientChatStore.setMapView(longitude, latitude, zoom);
|
||||||
|
// setMapView({ longitude, latitude, zoom });
|
||||||
|
|
||||||
|
// Update the store with the new view state
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pins = useMemo(
|
const pins = useMemo(
|
||||||
@@ -98,13 +113,15 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
|||||||
{/* </Button>*/}
|
{/* </Button>*/}
|
||||||
{/*</HStack>*/}
|
{/*</HStack>*/}
|
||||||
<Map
|
<Map
|
||||||
|
ref={mapRef}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
latitude: 40,
|
latitude: clientChatStore.mapState.latitude,
|
||||||
longitude: -100,
|
longitude: clientChatStore.mapState.longitude,
|
||||||
zoom: 3.5,
|
zoom: clientChatStore.mapState.zoom,
|
||||||
bearing: 0,
|
bearing: clientChatStore.mapState.bearing,
|
||||||
pitch: 0,
|
pitch: clientChatStore.mapState.pitch,
|
||||||
}}
|
}}
|
||||||
|
onMove={handleMapViewChange}
|
||||||
mapStyle="mapbox://styles/geoffsee/cmd1qz39x01ga01qv5acea02y"
|
mapStyle="mapbox://styles/geoffsee/cmd1qz39x01ga01qv5acea02y"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
mapboxAccessToken={props.mapboxPublicKey}
|
mapboxAccessToken={props.mapboxPublicKey}
|
||||||
@@ -170,3 +187,6 @@ Type '{ city: string; population: string; image: string; state: string; latitude
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MapNext = observer(MapNextComponent);
|
||||||
|
export default MapNext;
|
||||||
|
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface SliderControl {
|
interface SliderControl {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -34,6 +34,8 @@ interface SwitchControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TweakboxProps {
|
interface TweakboxProps {
|
||||||
|
id: string;
|
||||||
|
persist: boolean;
|
||||||
sliders: {
|
sliders: {
|
||||||
speed: SliderControl;
|
speed: SliderControl;
|
||||||
intensity: SliderControl;
|
intensity: SliderControl;
|
||||||
@@ -44,7 +46,7 @@ interface TweakboxProps {
|
|||||||
} & Record<string, SwitchControl>;
|
} & Record<string, SwitchControl>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tweakbox = observer(({ sliders, switches }: TweakboxProps) => {
|
const Tweakbox = observer(({ id, persist, sliders, switches }: TweakboxProps) => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box, useMediaQuery } 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';
|
||||||
@@ -21,10 +21,11 @@ export default function IndexPage() {
|
|||||||
|
|
||||||
const component = useComponent();
|
const component = useComponent();
|
||||||
|
|
||||||
|
const mediaQuery = useMediaQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box height="100%" width="100%">
|
<Box height="100%" width="100%">
|
||||||
<LandingComponent />
|
<LandingComponent />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display={component.enabledComponent === 'ai' ? undefined : 'none'}
|
display={component.enabledComponent === 'ai' ? undefined : 'none'}
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -36,8 +37,8 @@ export default function IndexPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
display={component.enabledComponent === 'gpsmap' ? undefined : 'none'}
|
display={component.enabledComponent === 'gpsmap' ? undefined : 'none'}
|
||||||
width="100%"
|
width={{ base: '100%', md: '100%' }}
|
||||||
height="100%"
|
height={{ base: '100%', md: '100%' }}
|
||||||
padding={'unset'}
|
padding={'unset'}
|
||||||
>
|
>
|
||||||
<ReactMap visible={component.enabledComponent === 'gpsmap'} />
|
<ReactMap visible={component.enabledComponent === 'gpsmap'} />
|
||||||
|
@@ -3,13 +3,14 @@
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
import { types, type Instance } from 'mobx-state-tree';
|
import { types, type Instance } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
import { MapStore } from './MapStore';
|
||||||
import { MessagesStore } from './MessagesStore';
|
import { MessagesStore } from './MessagesStore';
|
||||||
import { ModelStore } from './ModelStore';
|
import { ModelStore } from './ModelStore';
|
||||||
import { StreamStore } from './StreamStore';
|
import { StreamStore } from './StreamStore';
|
||||||
import { UIStore } from './UIStore';
|
import { UIStore } from './UIStore';
|
||||||
|
|
||||||
export const ClientChatStore = types
|
export const ClientChatStore = types
|
||||||
.compose(MessagesStore, UIStore, ModelStore, StreamStore)
|
.compose(MessagesStore, UIStore, ModelStore, StreamStore, MapStore)
|
||||||
.named('ClientChatStore');
|
.named('ClientChatStore');
|
||||||
|
|
||||||
const clientChatStore = ClientChatStore.create();
|
const clientChatStore = ClientChatStore.create();
|
||||||
|
117
packages/client/src/stores/MapStore.ts
Normal file
117
packages/client/src/stores/MapStore.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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
|
||||||
|
longitude: types.optional(types.number, -122.4),
|
||||||
|
latitude: types.optional(types.number, 37.8),
|
||||||
|
zoom: types.optional(types.number, 14),
|
||||||
|
// Map control state
|
||||||
|
isControlActive: types.optional(types.boolean, false),
|
||||||
|
})
|
||||||
|
.volatile(self => ({
|
||||||
|
// Store pending map commands from AI
|
||||||
|
pendingCommands: [] as MapControlCommand[],
|
||||||
|
mapState: {
|
||||||
|
latitude: self.latitude,
|
||||||
|
longitude: self.longitude,
|
||||||
|
zoom: self.zoom,
|
||||||
|
bearing: 0,
|
||||||
|
pitch: 0,
|
||||||
|
} as any,
|
||||||
|
}))
|
||||||
|
.actions(self => ({
|
||||||
|
// Update map view state
|
||||||
|
setMapView(longitude: number, latitude: number, zoom: number) {
|
||||||
|
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,6 +2,8 @@ import { flow, getParent, type Instance, types } from 'mobx-state-tree';
|
|||||||
|
|
||||||
import Message, { batchContentUpdate } from '../models/Message';
|
import Message, { batchContentUpdate } from '../models/Message';
|
||||||
|
|
||||||
|
import clientChatStore from './ClientChatStore.ts';
|
||||||
|
import type { MapControlCommand } from './MapStore';
|
||||||
import type { RootDeps } from './RootDeps.ts';
|
import type { RootDeps } from './RootDeps.ts';
|
||||||
import UserOptionsStore from './UserOptionsStore';
|
import UserOptionsStore from './UserOptionsStore';
|
||||||
|
|
||||||
@@ -92,6 +94,30 @@ export const StreamStore = types
|
|||||||
return;
|
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
|
// Get the last message
|
||||||
const lastMessage = root.items[root.items.length - 1];
|
const lastMessage = root.items[root.items.length - 1];
|
||||||
|
|
||||||
@@ -152,4 +178,4 @@ export const StreamStore = types
|
|||||||
return { sendMessage, stopIncomingMessage, cleanup, setEventSource, setStreamId };
|
return { sendMessage, stopIncomingMessage, cleanup, setEventSource, setStreamId };
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface IStreamStore extends Instance<typeof StreamStore> {}
|
export type IStreamStore = Instance<typeof StreamStore>;
|
||||||
|
@@ -183,7 +183,7 @@ const ChatService = types
|
|||||||
modelMeta.set(mdl.id, { ...mdl, ...meta });
|
modelMeta.set(mdl.id, { ...mdl, ...meta });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// logger.error(`Metadata fetch failed for ${mdl.id}`, 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) {
|
} catch (err) {
|
||||||
@@ -277,8 +277,23 @@ const ChatService = types
|
|||||||
}) {
|
}) {
|
||||||
const { streamConfig, streamParams, controller, encoder, streamId } = params;
|
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);
|
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 = () => {
|
const useModelHandler = () => {
|
||||||
// @ts-expect-error - language server does not have enough information to validate modelFamily as an indexer for modelHandlers
|
// @ts-expect-error - language server does not have enough information to validate modelFamily as an indexer for modelHandlers
|
||||||
return modelHandlers[modelFamily];
|
return modelHandlers[modelFamily];
|
||||||
@@ -287,9 +302,28 @@ const ChatService = types
|
|||||||
const handler = useModelHandler();
|
const handler = useModelHandler();
|
||||||
|
|
||||||
if (handler) {
|
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 {
|
try {
|
||||||
await handler(streamParams, Common.Utils.handleStreamData(controller, encoder));
|
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) {
|
} 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();
|
const message = error.message.toLowerCase();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -318,10 +352,80 @@ const ChatService = types
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (message.includes('404')) {
|
if (message.includes('404')) {
|
||||||
throw new ClientError(`Something went wrong, try again.`, 413, {});
|
// 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
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),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -333,11 +437,27 @@ const ChatService = types
|
|||||||
}) {
|
}) {
|
||||||
const { streamId, streamConfig, savedStreamConfig, durableObject } = params;
|
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({
|
return new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
try {
|
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);
|
const dynamicContext = Schema.Message.create(streamConfig.preprocessedContext);
|
||||||
|
|
||||||
// Process the stream data using the appropriate handler
|
// Process the stream data using the appropriate handler
|
||||||
@@ -347,6 +467,16 @@ const ChatService = types
|
|||||||
durableObject,
|
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({
|
await self.runModelHandler({
|
||||||
streamConfig,
|
streamConfig,
|
||||||
streamParams,
|
streamParams,
|
||||||
@@ -355,6 +485,11 @@ const ChatService = types
|
|||||||
streamId,
|
streamId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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);
|
console.error(`chatService::handleSseStream::${streamId}::Error`, error);
|
||||||
|
|
||||||
if (error instanceof ClientError) {
|
if (error instanceof ClientError) {
|
||||||
@@ -376,6 +511,10 @@ const ChatService = types
|
|||||||
controller.close();
|
controller.close();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log(
|
||||||
|
`[DEBUG_LOG] ChatService.createSseReadableStream: Closing stream ${streamId}`,
|
||||||
|
);
|
||||||
controller.close();
|
controller.close();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore errors when closing the controller, as it might already be closed
|
// Ignore errors when closing the controller, as it might already be closed
|
||||||
@@ -388,21 +527,53 @@ const ChatService = types
|
|||||||
handleSseStream: flow(function* (
|
handleSseStream: flow(function* (
|
||||||
streamId: string,
|
streamId: string,
|
||||||
): Generator<Promise<string>, Response, unknown> {
|
): 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
|
// Check if a stream is already active for this ID
|
||||||
if (self.activeStreams.has(streamId)) {
|
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 });
|
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
|
// Retrieve the stream configuration from the durable object
|
||||||
const objectId = self.env.SERVER_COORDINATOR.idFromName('stream-index');
|
const objectId = self.env.SERVER_COORDINATOR.idFromName('stream-index');
|
||||||
const durableObject = self.env.SERVER_COORDINATOR.get(objectId);
|
const durableObject = self.env.SERVER_COORDINATOR.get(objectId);
|
||||||
const savedStreamConfig: any = yield durableObject.getStreamData(streamId);
|
const savedStreamConfig: any = yield durableObject.getStreamData(streamId);
|
||||||
|
|
||||||
if (!savedStreamConfig) {
|
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 });
|
return new Response('Stream not found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamConfig = JSON.parse(savedStreamConfig);
|
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({
|
const stream = self.createSseReadableStream({
|
||||||
streamId,
|
streamId,
|
||||||
@@ -414,18 +585,37 @@ const ChatService = types
|
|||||||
// Use `tee()` to create two streams: one for processing and one for the response
|
// Use `tee()` to create two streams: one for processing and one for the response
|
||||||
const [processingStream, responseStream] = stream.tee();
|
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, {
|
self.setActiveStream(streamId, {
|
||||||
...streamConfig,
|
...streamConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log(
|
||||||
|
`[DEBUG_LOG] ChatService.handleSseStream: Setting up processing stream pipeline for ${streamId}`,
|
||||||
|
);
|
||||||
|
|
||||||
processingStream.pipeTo(
|
processingStream.pipeTo(
|
||||||
new WritableStream({
|
new WritableStream({
|
||||||
close() {
|
close() {
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
console.log(
|
||||||
|
`[DEBUG_LOG] ChatService.handleSseStream: Processing stream closed for ${streamId}, removing active stream`,
|
||||||
|
);
|
||||||
self.removeActiveStream(streamId);
|
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 the second stream as the response
|
||||||
return new Response(responseStream, {
|
return new Response(responseStream, {
|
||||||
headers: {
|
headers: {
|
||||||
|
Reference in New Issue
Block a user