From d490174544765a68fb87233063e401b69b039cb9 Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Fri, 5 Sep 2025 02:18:30 -0700 Subject: [PATCH] feat: Web use jan model (#6374) * call jan api * fix lint * ci: add jan server web * chore: add Dockerfile * clean up ui ux and support for reasoning fields, make app spa * add logo * chore: update tag for preview image * chore: update k8s service name * chore: update image tag and image name * fixed test --------- Co-authored-by: Minh141120 Co-authored-by: Nguyen Ngoc Minh <91668012+Minh141120@users.noreply.github.com> --- .github/workflows/jan-server-web-ci.yml | 117 +++++++ Dockerfile | 48 +++ extensions-web/package.json | 10 +- extensions-web/src/index.ts | 5 +- extensions-web/src/jan-provider-web/api.ts | 260 +++++++++++++++ extensions-web/src/jan-provider-web/auth.ts | 190 +++++++++++ extensions-web/src/jan-provider-web/index.ts | 1 + .../src/jan-provider-web/provider.ts | 307 ++++++++++++++++++ extensions-web/src/jan-provider-web/store.ts | 95 ++++++ extensions-web/src/types.ts | 9 +- extensions-web/src/types/global.d.ts | 5 + extensions-web/src/vite-env.d.ts | 1 + extensions-web/vite.config.ts | 10 +- web-app/index.html | 4 +- web-app/package.json | 3 +- web-app/public/images/jan-logo.png | Bin 0 -> 36896 bytes web-app/public/images/model-provider/jan.png | Bin 0 -> 36896 bytes .../src/containers/DropdownModelProvider.tsx | 57 +++- web-app/src/containers/LeftPanel.tsx | 2 +- web-app/src/containers/SettingsMenu.tsx | 6 +- web-app/src/containers/SetupScreen.tsx | 2 +- web-app/src/lib/platform/const.ts | 12 + web-app/src/lib/platform/types.ts | 12 + web-app/src/lib/utils.ts | 4 + web-app/src/providers/AnalyticProvider.tsx | 6 + web-app/src/routes/__root.tsx | 4 +- web-app/src/routes/index.tsx | 5 +- web-app/src/routes/settings/general.tsx | 54 +-- web-app/src/routes/settings/privacy.tsx | 6 +- .../settings/providers/$providerName.tsx | 26 ++ .../src/routes/settings/providers/index.tsx | 26 ++ web-app/src/services/providers/web.ts | 91 +++--- web-app/src/test/setup.ts | 42 ++- web-app/vite.config.web.ts | 2 + 34 files changed, 1309 insertions(+), 113 deletions(-) create mode 100644 .github/workflows/jan-server-web-ci.yml create mode 100644 Dockerfile create mode 100644 extensions-web/src/jan-provider-web/api.ts create mode 100644 extensions-web/src/jan-provider-web/auth.ts create mode 100644 extensions-web/src/jan-provider-web/index.ts create mode 100644 extensions-web/src/jan-provider-web/provider.ts create mode 100644 extensions-web/src/jan-provider-web/store.ts create mode 100644 extensions-web/src/types/global.d.ts create mode 100644 extensions-web/src/vite-env.d.ts create mode 100644 web-app/public/images/jan-logo.png create mode 100644 web-app/public/images/model-provider/jan.png diff --git a/.github/workflows/jan-server-web-ci.yml b/.github/workflows/jan-server-web-ci.yml new file mode 100644 index 000000000..d65fa7b9a --- /dev/null +++ b/.github/workflows/jan-server-web-ci.yml @@ -0,0 +1,117 @@ +name: Jan Web Server build image and push to Harbor Registry + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + build-and-preview: + runs-on: [ubuntu-24-04-docker] + permissions: + pull-requests: write + contents: write + steps: + - name: Checkout source repo + uses: actions/checkout@v4 + + - name: Login to Harbor Registry + uses: docker/login-action@v3 + with: + registry: registry.menlo.ai + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Install dependencies + run: | + (type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && sudo mkdir -p -m 755 /etc/apt/sources.list.d \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + sudo apt-get update + sudo apt-get install -y jq gettext + + - name: Set image tag and service name + id: vars + run: | + SERVICE_NAME=jan-server-web + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + IMAGE_TAG="web:preview-${{ github.sha }}" + else + IMAGE_TAG="web:dev-${{ github.sha }}" + fi + echo "SERVICE_NAME=${SERVICE_NAME}" >> $GITHUB_OUTPUT + echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "FULL_IMAGE=registry.menlo.ai/jan-server/${IMAGE_TAG}" >> $GITHUB_OUTPUT + + - name: Build docker image + run: | + docker build -t ${{ steps.vars.outputs.FULL_IMAGE }} . + + - name: Push docker image + run: | + docker push ${{ steps.vars.outputs.FULL_IMAGE }} + + - name: Checkout preview URL repo + if: github.event_name == 'pull_request' + uses: actions/checkout@v4 + with: + repository: menloresearch/infra-domains + token: ${{ secrets.PAT_SERVICE_ACCOUNT }} + path: preview-repo + + - name: Generate preview manifest + if: github.event_name == 'pull_request' + run: | + cd preview-repo/kubernetes + bash template/generate.sh \ + template/preview-url-template.yaml \ + preview-url/pr-${{ github.sha }}-${{ steps.vars.outputs.SERVICE_NAME }}.yaml \ + ${{ github.sha }} \ + ${{ steps.vars.outputs.SERVICE_NAME }} \ + ${{ steps.vars.outputs.FULL_IMAGE }} \ + 80 + + - name: Commit and push preview manifest + if: github.event_name == 'pull_request' + run: | + cd preview-repo + git config user.name "preview-bot" + git config user.email "preview-bot@users.noreply.github.com" + git add kubernetes/preview-url/pr-${{ github.sha }}-${{ steps.vars.outputs.SERVICE_NAME }}.yaml + git commit -m "feat(preview): add pr-${{ github.sha }}-${{ steps.vars.outputs.SERVICE_NAME }}.yaml" + git push origin main + sleep 180 + + - name: Comment preview URL on PR + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + DOMAIN="https://pr-${{ github.sha }}-${{ steps.vars.outputs.SERVICE_NAME }}.menlo.ai" + COMMENT_BODY="🌐 Preview available: [${DOMAIN}](${DOMAIN})" + + echo "🔍 Looking for existing preview comment..." + + COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ + --jq '.[] | select(.user.login=="github-actions[bot]") | select(.body | contains("")) | .id') + + if [[ -n "$COMMENT_ID" ]]; then + echo "✏️ Updating existing comment ID $COMMENT_ID" + gh api repos/${{ github.repository }}/issues/comments/${COMMENT_ID} \ + --method PATCH \ + --field "body=${COMMENT_BODY}" + else + echo "💬 Creating new comment" + gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ + --method POST \ + --field "body=${COMMENT_BODY}" + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..840ae887f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Stage 1: Build stage with Node.js and Yarn v4 +FROM node:20-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache \ + make \ + g++ \ + python3 \ + py3-pip \ + git + +# Enable corepack and install Yarn 4 +RUN corepack enable && corepack prepare yarn@4.5.3 --activate + +# Verify Yarn version +RUN yarn --version + +# Set working directory +WORKDIR /app + +# Copy source code +COPY ./extensions ./extensions +COPY ./extensions-web ./extensions-web +COPY ./web-app ./web-app +COPY ./Makefile ./Makefile +COPY ./.* / +COPY ./package.json ./package.json +COPY ./yarn.lock ./yarn.lock +COPY ./pre-install ./pre-install +COPY ./core ./core + +# Build web application +RUN yarn install && yarn build:core && make build-web-app + +# Stage 2: Production stage with Nginx +FROM nginx:alpine + +# Copy static files from build stage +COPY --from=builder /app/web-app/dist-web /usr/share/nginx/html + +# Copy nginx configuration for SPA (if custom config is needed) +# COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/extensions-web/package.json b/extensions-web/package.json index 2e43b296d..8d54443fe 100644 --- a/extensions-web/package.json +++ b/extensions-web/package.json @@ -20,15 +20,15 @@ "test": "vitest", "typecheck": "tsc --noEmit" }, - "dependencies": { - "@janhq/core": "workspace:*" - }, "devDependencies": { + "@janhq/core": "workspace:*", "typescript": "^5.3.3", "vite": "^5.0.0", - "vitest": "^2.0.0" + "vitest": "^2.0.0", + "zustand": "^5.0.8" }, "peerDependencies": { - "@janhq/core": "*" + "@janhq/core": "*", + "zustand": "^5.0.0" } } diff --git a/extensions-web/src/index.ts b/extensions-web/src/index.ts index 72b507823..aa53e37e1 100644 --- a/extensions-web/src/index.ts +++ b/extensions-web/src/index.ts @@ -7,6 +7,7 @@ import type { WebExtensionRegistry } from './types' export { default as AssistantExtensionWeb } from './assistant-web' export { default as ConversationalExtensionWeb } from './conversational-web' +export { default as JanProviderWeb } from './jan-provider-web' // Re-export types export type { @@ -15,11 +16,13 @@ export type { WebExtensionName, WebExtensionLoader, AssistantWebModule, - ConversationalWebModule + ConversationalWebModule, + JanProviderWebModule } from './types' // Extension registry for dynamic loading export const WEB_EXTENSIONS: WebExtensionRegistry = { 'assistant-web': () => import('./assistant-web'), 'conversational-web': () => import('./conversational-web'), + 'jan-provider-web': () => import('./jan-provider-web'), } \ No newline at end of file diff --git a/extensions-web/src/jan-provider-web/api.ts b/extensions-web/src/jan-provider-web/api.ts new file mode 100644 index 000000000..68dd6cb77 --- /dev/null +++ b/extensions-web/src/jan-provider-web/api.ts @@ -0,0 +1,260 @@ +/** + * Jan Provider API Client + * Handles API requests to Jan backend for models and chat completions + */ + +import { JanAuthService } from './auth' +import { JanModel, janProviderStore } from './store' + +// JAN_API_BASE is defined in vite.config.ts + +export interface JanModelsResponse { + object: string + data: JanModel[] +} + +export interface JanChatMessage { + role: 'system' | 'user' | 'assistant' + content: string + reasoning?: string + reasoning_content?: string +} + +export interface JanChatCompletionRequest { + model: string + messages: JanChatMessage[] + temperature?: number + max_tokens?: number + top_p?: number + frequency_penalty?: number + presence_penalty?: number + stream?: boolean + stop?: string | string[] +} + +export interface JanChatCompletionChoice { + index: number + message: JanChatMessage + finish_reason: string | null +} + +export interface JanChatCompletionResponse { + id: string + object: string + created: number + model: string + choices: JanChatCompletionChoice[] + usage?: { + prompt_tokens: number + completion_tokens: number + total_tokens: number + } +} + +export interface JanChatCompletionChunk { + id: string + object: string + created: number + model: string + choices: Array<{ + index: number + delta: { + role?: string + content?: string + reasoning?: string + reasoning_content?: string + } + finish_reason: string | null + }> +} + +export class JanApiClient { + private static instance: JanApiClient + private authService: JanAuthService + + private constructor() { + this.authService = JanAuthService.getInstance() + } + + static getInstance(): JanApiClient { + if (!JanApiClient.instance) { + JanApiClient.instance = new JanApiClient() + } + return JanApiClient.instance + } + + private async makeAuthenticatedRequest( + url: string, + options: RequestInit = {} + ): Promise { + try { + const authHeader = await this.authService.getAuthHeader() + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...authHeader, + ...options.headers, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + return response.json() + } catch (error) { + console.error('API request failed:', error) + throw error + } + } + + async getModels(): Promise { + try { + janProviderStore.setLoadingModels(true) + janProviderStore.clearError() + + const response = await this.makeAuthenticatedRequest( + `${JAN_API_BASE}/models` + ) + + const models = response.data || [] + janProviderStore.setModels(models) + + return models + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to fetch models' + janProviderStore.setError(errorMessage) + janProviderStore.setLoadingModels(false) + throw error + } + } + + async createChatCompletion( + request: JanChatCompletionRequest + ): Promise { + try { + janProviderStore.clearError() + + return await this.makeAuthenticatedRequest( + `${JAN_API_BASE}/chat/completions`, + { + method: 'POST', + body: JSON.stringify({ + ...request, + stream: false, + }), + } + ) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to create chat completion' + janProviderStore.setError(errorMessage) + throw error + } + } + + async createStreamingChatCompletion( + request: JanChatCompletionRequest, + onChunk: (chunk: JanChatCompletionChunk) => void, + onComplete?: () => void, + onError?: (error: Error) => void + ): Promise { + try { + janProviderStore.clearError() + + const authHeader = await this.authService.getAuthHeader() + + const response = await fetch(`${JAN_API_BASE}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeader, + }, + body: JSON.stringify({ + ...request, + stream: true, + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + if (!response.body) { + throw new Error('Response body is null') + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + + try { + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + + // Keep the last incomplete line in buffer + buffer = lines.pop() || '' + + for (const line of lines) { + const trimmedLine = line.trim() + if (trimmedLine.startsWith('data: ')) { + const data = trimmedLine.slice(6).trim() + + if (data === '[DONE]') { + onComplete?.() + return + } + + try { + const parsedChunk: JanChatCompletionChunk = JSON.parse(data) + onChunk(parsedChunk) + } catch (parseError) { + console.warn('Failed to parse SSE chunk:', parseError, 'Data:', data) + } + } + } + } + + onComplete?.() + } finally { + reader.releaseLock() + } + } catch (error) { + const err = error instanceof Error ? error : new Error('Unknown error occurred') + janProviderStore.setError(err.message) + onError?.(err) + throw err + } + } + + async initialize(): Promise { + try { + await this.authService.initialize() + janProviderStore.setAuthenticated(true) + + // Fetch initial models + await this.getModels() + + console.log('Jan API client initialized successfully') + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to initialize API client' + janProviderStore.setError(errorMessage) + throw error + } finally { + janProviderStore.setInitializing(false) + } + } +} + +export const janApiClient = JanApiClient.getInstance() \ No newline at end of file diff --git a/extensions-web/src/jan-provider-web/auth.ts b/extensions-web/src/jan-provider-web/auth.ts new file mode 100644 index 000000000..be830c0d0 --- /dev/null +++ b/extensions-web/src/jan-provider-web/auth.ts @@ -0,0 +1,190 @@ +/** + * Jan Provider Authentication Service + * Handles guest login and token refresh for Jan API + */ + +export interface AuthTokens { + access_token: string + expires_in: number +} + +export interface AuthResponse { + access_token: string + expires_in: number +} + +// JAN_API_BASE is defined in vite.config.ts +const AUTH_STORAGE_KEY = 'jan_auth_tokens' +const TOKEN_EXPIRY_BUFFER = 60 * 1000 // 1 minute buffer before actual expiry + +export class JanAuthService { + private static instance: JanAuthService + private tokens: AuthTokens | null = null + private tokenExpiryTime: number = 0 + + private constructor() { + this.loadTokensFromStorage() + } + + static getInstance(): JanAuthService { + if (!JanAuthService.instance) { + JanAuthService.instance = new JanAuthService() + } + return JanAuthService.instance + } + + private loadTokensFromStorage(): void { + try { + const storedTokens = localStorage.getItem(AUTH_STORAGE_KEY) + if (storedTokens) { + const parsed = JSON.parse(storedTokens) + this.tokens = parsed.tokens + this.tokenExpiryTime = parsed.expiryTime || 0 + } + } catch (error) { + console.warn('Failed to load tokens from storage:', error) + this.clearTokens() + } + } + + private saveTokensToStorage(): void { + if (this.tokens) { + try { + localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ + tokens: this.tokens, + expiryTime: this.tokenExpiryTime + })) + } catch (error) { + console.error('Failed to save tokens to storage:', error) + } + } + } + + private clearTokens(): void { + this.tokens = null + this.tokenExpiryTime = 0 + localStorage.removeItem(AUTH_STORAGE_KEY) + } + + private isTokenExpired(): boolean { + return Date.now() > (this.tokenExpiryTime - TOKEN_EXPIRY_BUFFER) + } + + private calculateExpiryTime(expiresIn: number): number { + return Date.now() + (expiresIn * 1000) + } + + private async guestLogin(): Promise { + try { + const response = await fetch(`${JAN_API_BASE}/auth/guest-login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // Include cookies for session management + }) + + if (!response.ok) { + throw new Error(`Guest login failed: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + + // API response is wrapped in result object + const authResponse = data.result || data + + // Guest login returns only access_token and expires_in + const tokens: AuthTokens = { + access_token: authResponse.access_token, + expires_in: authResponse.expires_in + } + + this.tokens = tokens + this.tokenExpiryTime = this.calculateExpiryTime(tokens.expires_in) + this.saveTokensToStorage() + + return tokens + } catch (error) { + console.error('Guest login failed:', error) + throw error + } + } + + private async refreshToken(): Promise { + try { + const response = await fetch(`${JAN_API_BASE}/auth/refresh-token`, { + method: 'GET', + credentials: 'include', // Cookies will include the refresh token + }) + + if (!response.ok) { + if (response.status === 401) { + // Refresh token is invalid, clear tokens and do guest login + this.clearTokens() + return this.guestLogin() + } + throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + + // API response is wrapped in result object + const authResponse = data.result || data + + // Refresh endpoint returns only access_token and expires_in + const tokens: AuthTokens = { + access_token: authResponse.access_token, + expires_in: authResponse.expires_in + } + + this.tokens = tokens + this.tokenExpiryTime = this.calculateExpiryTime(tokens.expires_in) + this.saveTokensToStorage() + + return tokens + } catch (error) { + console.error('Token refresh failed:', error) + // If refresh fails, fall back to guest login + this.clearTokens() + return this.guestLogin() + } + } + + async getValidAccessToken(): Promise { + // If no tokens exist, do guest login + if (!this.tokens) { + const tokens = await this.guestLogin() + return tokens.access_token + } + + // If token is expired or about to expire, refresh it + if (this.isTokenExpired()) { + const tokens = await this.refreshToken() + return tokens.access_token + } + + // Return existing valid token + return this.tokens.access_token + } + + async initialize(): Promise { + try { + await this.getValidAccessToken() + console.log('Jan auth service initialized successfully') + } catch (error) { + console.error('Failed to initialize Jan auth service:', error) + throw error + } + } + + async getAuthHeader(): Promise<{ Authorization: string }> { + const token = await this.getValidAccessToken() + return { + Authorization: `Bearer ${token}` + } + } + + logout(): void { + this.clearTokens() + } +} \ No newline at end of file diff --git a/extensions-web/src/jan-provider-web/index.ts b/extensions-web/src/jan-provider-web/index.ts new file mode 100644 index 000000000..70cbf7770 --- /dev/null +++ b/extensions-web/src/jan-provider-web/index.ts @@ -0,0 +1 @@ +export { default } from './provider' \ No newline at end of file diff --git a/extensions-web/src/jan-provider-web/provider.ts b/extensions-web/src/jan-provider-web/provider.ts new file mode 100644 index 000000000..dbe39beba --- /dev/null +++ b/extensions-web/src/jan-provider-web/provider.ts @@ -0,0 +1,307 @@ +/** + * Jan Provider Extension for Web + * Provides remote model inference through Jan API + */ + +import { + AIEngine, + modelInfo, + SessionInfo, + UnloadResult, + chatCompletionRequest, + chatCompletion, + chatCompletionChunk, + ImportOptions, +} from '@janhq/core' // cspell: disable-line +import { janApiClient, JanChatMessage } from './api' +import { janProviderStore } from './store' + +export default class JanProviderWeb extends AIEngine { + readonly provider = 'jan' + private activeSessions: Map = new Map() + + override async onLoad() { + console.log('Loading Jan Provider Extension...') + + try { + // Initialize authentication and fetch models + await janApiClient.initialize() + console.log('Jan Provider Extension loaded successfully') + } catch (error) { + console.error('Failed to load Jan Provider Extension:', error) + throw error + } + + super.onLoad() + } + + override async onUnload() { + console.log('Unloading Jan Provider Extension...') + + // Clear all sessions + for (const sessionId of this.activeSessions.keys()) { + await this.unload(sessionId) + } + + janProviderStore.reset() + console.log('Jan Provider Extension unloaded') + } + + async list(): Promise { + try { + const janModels = await janApiClient.getModels() + + return janModels.map((model) => ({ + id: model.id, + name: model.id, // Use ID as name for now + quant_type: undefined, + providerId: this.provider, + port: 443, // HTTPS port for API + sizeBytes: 0, // Size not provided by Jan API + tags: [], + path: undefined, // Remote model, no local path + owned_by: model.owned_by, + object: model.object, + })) + } catch (error) { + console.error('Failed to list Jan models:', error) + throw error + } + } + + async load(modelId: string, _settings?: any): Promise { + try { + // For Jan API, we don't actually "load" models in the traditional sense + // We just create a session reference for tracking + const sessionId = `jan-${modelId}-${Date.now()}` + + const sessionInfo: SessionInfo = { + pid: Date.now(), // Use timestamp as pseudo-PID + port: 443, // HTTPS port + model_id: modelId, + model_path: `remote:${modelId}`, // Indicate this is a remote model + api_key: '', // API key handled by auth service + } + + this.activeSessions.set(sessionId, sessionInfo) + + console.log(`Jan model session created: ${sessionId} for model ${modelId}`) + return sessionInfo + } catch (error) { + console.error(`Failed to load Jan model ${modelId}:`, error) + throw error + } + } + + async unload(sessionId: string): Promise { + try { + const session = this.activeSessions.get(sessionId) + + if (!session) { + return { + success: false, + error: `Session ${sessionId} not found` + } + } + + this.activeSessions.delete(sessionId) + console.log(`Jan model session unloaded: ${sessionId}`) + + return { success: true } + } catch (error) { + console.error(`Failed to unload Jan session ${sessionId}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + } + } + } + + async chat( + opts: chatCompletionRequest, + abortController?: AbortController + ): Promise> { + try { + // Check if request was aborted before starting + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + // For Jan API, we need to determine which model to use + // The model should be specified in opts.model + const modelId = opts.model + if (!modelId) { + throw new Error('Model ID is required') + } + + // Convert core chat completion request to Jan API format + const janMessages: JanChatMessage[] = opts.messages.map(msg => ({ + role: msg.role as 'system' | 'user' | 'assistant', + content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + })) + + const janRequest = { + model: modelId, + messages: janMessages, + temperature: opts.temperature ?? undefined, + max_tokens: opts.n_predict ?? undefined, + top_p: opts.top_p ?? undefined, + frequency_penalty: opts.frequency_penalty ?? undefined, + presence_penalty: opts.presence_penalty ?? undefined, + stream: opts.stream ?? false, + stop: opts.stop ?? undefined, + } + + if (opts.stream) { + // Return async generator for streaming + return this.createStreamingGenerator(janRequest, abortController) + } else { + // Return single response + const response = await janApiClient.createChatCompletion(janRequest) + + // Check if aborted after completion + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + return { + id: response.id, + object: 'chat.completion' as const, + created: response.created, + model: response.model, + choices: response.choices.map(choice => ({ + index: choice.index, + message: { + role: choice.message.role, + content: choice.message.content, + reasoning: choice.message.reasoning, + reasoning_content: choice.message.reasoning_content, + }, + finish_reason: (choice.finish_reason || 'stop') as 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call', + })), + usage: response.usage, + } + } + } catch (error) { + console.error('Jan chat completion failed:', error) + throw error + } + } + + private async *createStreamingGenerator(janRequest: any, abortController?: AbortController) { + let resolve: () => void + let reject: (error: Error) => void + const chunks: any[] = [] + let isComplete = false + let error: Error | null = null + + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + // Handle abort signal + const abortListener = () => { + error = new Error('Request was aborted') + reject(error) + } + + if (abortController?.signal) { + if (abortController.signal.aborted) { + throw new Error('Request was aborted') + } + abortController.signal.addEventListener('abort', abortListener) + } + + try { + // Start the streaming request + janApiClient.createStreamingChatCompletion( + janRequest, + (chunk) => { + if (abortController?.signal?.aborted) { + return + } + const streamChunk = { + id: chunk.id, + object: chunk.object, + created: chunk.created, + model: chunk.model, + choices: chunk.choices.map(choice => ({ + index: choice.index, + delta: { + role: choice.delta.role, + content: choice.delta.content, + reasoning: choice.delta.reasoning, + reasoning_content: choice.delta.reasoning_content, + }, + finish_reason: choice.finish_reason, + })), + } + chunks.push(streamChunk) + }, + () => { + isComplete = true + resolve() + }, + (err) => { + error = err + reject(err) + } + ) + + // Yield chunks as they arrive + let yieldedIndex = 0 + while (!isComplete && !error) { + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + while (yieldedIndex < chunks.length) { + yield chunks[yieldedIndex] + yieldedIndex++ + } + + // Wait a bit before checking again + await new Promise(resolve => setTimeout(resolve, 10)) + } + + // Yield any remaining chunks + while (yieldedIndex < chunks.length) { + yield chunks[yieldedIndex] + yieldedIndex++ + } + + if (error) { + throw error + } + + await promise + } finally { + // Clean up abort listener + if (abortController?.signal) { + abortController.signal.removeEventListener('abort', abortListener) + } + } + } + + async delete(modelId: string): Promise { + throw new Error(`Delete operation not supported for remote Jan API model: ${modelId}`) + } + + async import(modelId: string, _opts: ImportOptions): Promise { + throw new Error(`Import operation not supported for remote Jan API model: ${modelId}`) + } + + async abortImport(modelId: string): Promise { + throw new Error(`Abort import operation not supported for remote Jan API model: ${modelId}`) + } + + async getLoadedModels(): Promise { + return Array.from(this.activeSessions.values()).map(session => session.model_id) + } + + async isToolSupported(): Promise { + // Tools are not yet supported + return false + } +} \ No newline at end of file diff --git a/extensions-web/src/jan-provider-web/store.ts b/extensions-web/src/jan-provider-web/store.ts new file mode 100644 index 000000000..02cc70686 --- /dev/null +++ b/extensions-web/src/jan-provider-web/store.ts @@ -0,0 +1,95 @@ +/** + * Jan Provider Store + * Zustand-based state management for Jan provider authentication and models + */ + +import { create } from 'zustand' + +export interface JanModel { + id: string + object: string + owned_by: string +} + +export interface JanProviderState { + isAuthenticated: boolean + isInitializing: boolean + models: JanModel[] + isLoadingModels: boolean + error: string | null +} + +export interface JanProviderActions { + setAuthenticated: (isAuthenticated: boolean) => void + setInitializing: (isInitializing: boolean) => void + setModels: (models: JanModel[]) => void + setLoadingModels: (isLoadingModels: boolean) => void + setError: (error: string | null) => void + clearError: () => void + reset: () => void +} + +export type JanProviderStore = JanProviderState & JanProviderActions + +const initialState: JanProviderState = { + isAuthenticated: false, + isInitializing: true, + models: [], + isLoadingModels: false, + error: null, +} + +export const useJanProviderStore = create((set) => ({ + ...initialState, + + setAuthenticated: (isAuthenticated: boolean) => + set({ isAuthenticated, error: null }), + + setInitializing: (isInitializing: boolean) => + set({ isInitializing }), + + setModels: (models: JanModel[]) => + set({ models, isLoadingModels: false }), + + setLoadingModels: (isLoadingModels: boolean) => + set({ isLoadingModels }), + + setError: (error: string | null) => + set({ error }), + + clearError: () => + set({ error: null }), + + reset: () => + set({ + isAuthenticated: false, + isInitializing: false, + models: [], + isLoadingModels: false, + error: null, + }), +})) + +// Export a store instance for non-React usage +export const janProviderStore = { + // Store access methods + getState: useJanProviderStore.getState, + setState: useJanProviderStore.setState, + subscribe: useJanProviderStore.subscribe, + + // Direct action methods + setAuthenticated: (isAuthenticated: boolean) => + useJanProviderStore.getState().setAuthenticated(isAuthenticated), + setInitializing: (isInitializing: boolean) => + useJanProviderStore.getState().setInitializing(isInitializing), + setModels: (models: JanModel[]) => + useJanProviderStore.getState().setModels(models), + setLoadingModels: (isLoadingModels: boolean) => + useJanProviderStore.getState().setLoadingModels(isLoadingModels), + setError: (error: string | null) => + useJanProviderStore.getState().setError(error), + clearError: () => + useJanProviderStore.getState().clearError(), + reset: () => + useJanProviderStore.getState().reset(), +} \ No newline at end of file diff --git a/extensions-web/src/types.ts b/extensions-web/src/types.ts index 5511c4479..4b2ba583e 100644 --- a/extensions-web/src/types.ts +++ b/extensions-web/src/types.ts @@ -2,7 +2,7 @@ * Web Extension Types */ -import type { AssistantExtension, ConversationalExtension, BaseExtension } from '@janhq/core' +import type { AssistantExtension, ConversationalExtension, BaseExtension, AIEngine } from '@janhq/core' type ExtensionConstructorParams = ConstructorParameters @@ -14,11 +14,16 @@ export interface ConversationalWebModule { default: new (...args: ExtensionConstructorParams) => ConversationalExtension } -export type WebExtensionModule = AssistantWebModule | ConversationalWebModule +export interface JanProviderWebModule { + default: new (...args: ExtensionConstructorParams) => AIEngine +} + +export type WebExtensionModule = AssistantWebModule | ConversationalWebModule | JanProviderWebModule export interface WebExtensionRegistry { 'assistant-web': () => Promise 'conversational-web': () => Promise + 'jan-provider-web': () => Promise } export type WebExtensionName = keyof WebExtensionRegistry diff --git a/extensions-web/src/types/global.d.ts b/extensions-web/src/types/global.d.ts new file mode 100644 index 000000000..a6e82d759 --- /dev/null +++ b/extensions-web/src/types/global.d.ts @@ -0,0 +1,5 @@ +export {} + +declare global { + declare const JAN_API_BASE: string +} \ No newline at end of file diff --git a/extensions-web/src/vite-env.d.ts b/extensions-web/src/vite-env.d.ts new file mode 100644 index 000000000..151aa6856 --- /dev/null +++ b/extensions-web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/extensions-web/vite.config.ts b/extensions-web/vite.config.ts index f56e30df7..8d9147c79 100644 --- a/extensions-web/vite.config.ts +++ b/extensions-web/vite.config.ts @@ -9,13 +9,11 @@ export default defineConfig({ fileName: 'index' }, rollupOptions: { - external: ['@janhq/core'], - output: { - globals: { - '@janhq/core': 'JanCore' - } - } + external: ['@janhq/core', 'zustand'] }, emptyOutDir: false // Don't clean the output directory + }, + define: { + JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/jan/v1'), } }) \ No newline at end of file diff --git a/web-app/index.html b/web-app/index.html index a3117f37a..fc264d096 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -2,7 +2,9 @@ - + + + Jan diff --git a/web-app/package.json b/web-app/package.json index 53f1e46fc..2d080d0e6 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -13,7 +13,8 @@ "dev:web": "vite --config vite.config.web.ts", "build:web": "yarn tsc -b tsconfig.web.json && vite build --config vite.config.web.ts", "preview:web": "vite preview --config vite.config.web.ts --outDir dist-web", - "serve:web": "npx serve dist-web -p 3001", + "serve:web": "npx serve dist-web -p 3001 -s", + "serve:web:alt": "npx http-server dist-web -p 3001 --proxy http://localhost:3001? -o", "build:serve:web": "yarn build:web && yarn serve:web" }, "dependencies": { diff --git a/web-app/public/images/jan-logo.png b/web-app/public/images/jan-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c16023e946407fa823feefa362903dad081c5461 GIT binary patch literal 36896 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 literal 0 HcmV?d00001 diff --git a/web-app/public/images/model-provider/jan.png b/web-app/public/images/model-provider/jan.png new file mode 100644 index 0000000000000000000000000000000000000000..c16023e946407fa823feefa362903dad081c5461 GIT binary patch literal 36896 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 literal 0 HcmV?d00001 diff --git a/web-app/src/containers/DropdownModelProvider.tsx b/web-app/src/containers/DropdownModelProvider.tsx index da3bcd57b..303fe2a37 100644 --- a/web-app/src/containers/DropdownModelProvider.tsx +++ b/web-app/src/containers/DropdownModelProvider.tsx @@ -21,6 +21,8 @@ import { useTranslation } from '@/i18n/react-i18next-compat' import { useFavoriteModel } from '@/hooks/useFavoriteModel' import { predefinedProviders } from '@/consts/providers' import { useServiceHub } from '@/hooks/useServiceHub' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' type DropdownModelProviderProps = { model?: ThreadModel @@ -171,8 +173,28 @@ const DropdownModelProvider = ({ await checkAndUpdateModelVisionCapability(lastUsed.model) } } else { + // For web-only builds, auto-select the first model from jan provider + if (PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION]) { + const janProvider = providers.find( + (p) => p.provider === 'jan' && p.active && p.models.length > 0 + ) + if (janProvider && janProvider.models.length > 0) { + const firstModel = janProvider.models[0] + selectModelProvider(janProvider.provider, firstModel.id) + return + } + } selectModelProvider('', '') } + } else if (PlatformFeatures[PlatformFeature.WEB_AUTO_MODEL_SELECTION] && !selectedModel) { + // For web-only builds, always auto-select the first model from jan provider if none is selected + const janProvider = providers.find( + (p) => p.provider === 'jan' && p.active && p.models.length > 0 + ) + if (janProvider && janProvider.models.length > 0) { + const firstModel = janProvider.models[0] + selectModelProvider(janProvider.provider, firstModel.id) + } } } @@ -188,6 +210,7 @@ const DropdownModelProvider = ({ getProviderByName, checkAndUpdateModelVisionCapability, serviceHub, + selectedModel, ]) // Update display model when selection changes @@ -549,22 +572,24 @@ const DropdownModelProvider = ({ {getProviderTitle(providerInfo.provider)} -
{ - e.stopPropagation() - navigate({ - to: route.settings.providers, - params: { providerName: providerInfo.provider }, - }) - setOpen(false) - }} - > - -
+ {PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS] && ( +
{ + e.stopPropagation() + navigate({ + to: route.settings.providers, + params: { providerName: providerInfo.provider }, + }) + setOpen(false) + }} + > + +
+ )} {/* Models for this provider */} diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 39e1b2ca2..b019d318e 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -480,7 +480,7 @@ const LeftPanel = () => {
{mainMenus.map((menu) => { if (!menu.isEnabled) { - return <> + return null } const isActive = currentPath.includes(route.settings.index) && diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx index 52eebd741..7a543b03f 100644 --- a/web-app/src/containers/SettingsMenu.tsx +++ b/web-app/src/containers/SettingsMenu.tsx @@ -80,13 +80,13 @@ const SettingsMenu = () => { title: 'common:privacy', route: route.settings.privacy, hasSubMenu: false, - isEnabled: true, + isEnabled: PlatformFeatures[PlatformFeature.ANALYTICS], }, { title: 'common:modelProviders', route: route.settings.model_providers, hasSubMenu: activeProviders.length > 0, - isEnabled: true, + isEnabled: PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS], }, { title: 'common:keyboardShortcuts', @@ -159,7 +159,7 @@ const SettingsMenu = () => {
{menuSettings.map((menu) => { if (!menu.isEnabled) { - return <> + return null } return (
diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx index e9867b38a..812ed6493 100644 --- a/web-app/src/containers/SetupScreen.tsx +++ b/web-app/src/containers/SetupScreen.tsx @@ -11,7 +11,7 @@ function SetupScreen() { const { t } = useTranslation() const { providers } = useModelProvider() const firstItemRemoteProvider = - providers.length > 0 ? providers[1].provider : 'openai' + providers.length > 0 ? providers[1]?.provider : 'openai' // Check if setup tour has been completed const isSetupCompleted = diff --git a/web-app/src/lib/platform/const.ts b/web-app/src/lib/platform/const.ts index da62860f4..e2aabb109 100644 --- a/web-app/src/lib/platform/const.ts +++ b/web-app/src/lib/platform/const.ts @@ -34,4 +34,16 @@ export const PlatformFeatures: Record = { // HTTPS proxy [PlatformFeature.HTTPS_PROXY]: isPlatformTauri(), + + // Default model providers (OpenAI, Anthropic, etc.) - disabled for web-only Jan builds + [PlatformFeature.DEFAULT_PROVIDERS]: isPlatformTauri(), + + // Analytics and telemetry - disabled for web + [PlatformFeature.ANALYTICS]: isPlatformTauri(), + + // Web-specific automatic model selection from jan provider - enabled for web only + [PlatformFeature.WEB_AUTO_MODEL_SELECTION]: !isPlatformTauri(), + + // Model provider settings page management - disabled for web only + [PlatformFeature.MODEL_PROVIDER_SETTINGS]: isPlatformTauri(), } \ No newline at end of file diff --git a/web-app/src/lib/platform/types.ts b/web-app/src/lib/platform/types.ts index c12c797a9..5aa0fe7f4 100644 --- a/web-app/src/lib/platform/types.ts +++ b/web-app/src/lib/platform/types.ts @@ -36,4 +36,16 @@ export enum PlatformFeature { // HTTPS proxy HTTPS_PROXY = 'httpsProxy', + + // Default model providers (OpenAI, Anthropic, etc.) + DEFAULT_PROVIDERS = 'defaultProviders', + + // Analytics and telemetry + ANALYTICS = 'analytics', + + // Web-specific automatic model selection from jan provider + WEB_AUTO_MODEL_SELECTION = 'webAutoModelSelection', + + // Model provider settings page management + MODEL_PROVIDER_SETTINGS = 'modelProviderSettings', } diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index 3d896b883..0d3fa8f61 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -8,6 +8,8 @@ export function cn(...inputs: ClassValue[]) { export function getProviderLogo(provider: string) { switch (provider) { + case 'jan': + return '/images/model-provider/jan.png' case 'llamacpp': return '/images/model-provider/llamacpp.svg' case 'anthropic': @@ -33,6 +35,8 @@ export function getProviderLogo(provider: string) { export const getProviderTitle = (provider: string) => { switch (provider) { + case 'jan': + return 'Jan' case 'llamacpp': return 'Llama.cpp' case 'openai': diff --git a/web-app/src/providers/AnalyticProvider.tsx b/web-app/src/providers/AnalyticProvider.tsx index 29bd93074..f26e3f97e 100644 --- a/web-app/src/providers/AnalyticProvider.tsx +++ b/web-app/src/providers/AnalyticProvider.tsx @@ -3,12 +3,18 @@ import { useEffect } from 'react' import { useServiceHub } from '@/hooks/useServiceHub' import { useAnalytic } from '@/hooks/useAnalytic' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' export function AnalyticProvider() { const { productAnalytic } = useAnalytic() const serviceHub = useServiceHub() useEffect(() => { + // Early exit if analytics are disabled for this platform + if (!PlatformFeatures[PlatformFeature.ANALYTICS]) { + return + } if (!POSTHOG_KEY || !POSTHOG_HOST) { console.warn( 'PostHog not initialized: Missing POSTHOG_KEY or POSTHOG_HOST environment variables' diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 03435933d..13ed27813 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -31,6 +31,8 @@ import GlobalError from '@/containers/GlobalError' import { GlobalEventHandler } from '@/providers/GlobalEventHandler' import ErrorDialog from '@/containers/dialogs/ErrorDialog' import { ServiceHubProvider } from '@/providers/ServiceHubProvider' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' export const Route = createRootRoute({ component: RootLayout, @@ -162,7 +164,7 @@ const AppLayout = () => {
)} - {productAnalyticPrompt && } + {PlatformFeatures[PlatformFeature.ANALYTICS] && productAnalyticPrompt && } ) } diff --git a/web-app/src/routes/index.tsx b/web-app/src/routes/index.tsx index a8d9815ff..4ff643356 100644 --- a/web-app/src/routes/index.tsx +++ b/web-app/src/routes/index.tsx @@ -35,11 +35,12 @@ function Index() { useTools() // Conditional to check if there are any valid providers - // required min 1 api_key or 1 model in llama.cpp + // required min 1 api_key or 1 model in llama.cpp or jan provider const hasValidProviders = providers.some( (provider) => provider.api_key?.length || - (provider.provider === 'llamacpp' && provider.models.length) + (provider.provider === 'llamacpp' && provider.models.length) || + (provider.provider === 'jan' && provider.models.length) ) useEffect(() => { diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index 574d7ee6a..a07015c3f 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -181,14 +181,16 @@ function General() {
{/* General */} - - v{VERSION} - - } - /> + {PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && ( + + v{VERSION} + + } + /> + )} {!AUTO_UPDATER_DISABLED && PlatformFeatures[PlatformFeature.SYSTEM_INTEGRATIONS] && ( } /> - setHuggingfaceToken(e.target.value)} - placeholder={'hf_xxx'} - required - /> - } - /> + {PlatformFeatures[PlatformFeature.MODEL_HUB] && ( + setHuggingfaceToken(e.target.value)} + placeholder={'hf_xxx'} + required + /> + } + /> + )} {/* Resources */} diff --git a/web-app/src/routes/settings/privacy.tsx b/web-app/src/routes/settings/privacy.tsx index 425c4865a..3d0771db7 100644 --- a/web-app/src/routes/settings/privacy.tsx +++ b/web-app/src/routes/settings/privacy.tsx @@ -7,6 +7,8 @@ import { Card, CardItem } from '@/containers/Card' import { useTranslation } from '@/i18n/react-i18next-compat' import { useAnalytic } from '@/hooks/useAnalytic' import posthog from 'posthog-js' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.privacy as any)({ @@ -26,7 +28,8 @@ function Privacy() {
-

@@ -82,6 +85,7 @@ function Privacy() { } /> + )}

diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 8749bf915..873dc29b3 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -31,6 +31,8 @@ import { useCallback, useEffect, useState } from 'react' import { predefinedProviders } from '@/consts/providers' import { useModelLoad } from '@/hooks/useModelLoad' import { useLlamacppDevices } from '@/hooks/useLlamacppDevices' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' // as route.threadsDetail export const Route = createFileRoute('/settings/providers/$providerName')({ @@ -284,6 +286,30 @@ function ProviderDetail() { }) } + // Check if model provider settings are enabled for this platform + if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) { + return ( +
+ +

{t('common:settings')}

+
+
+ +
+
+

+ {t('common:notAvailable')} +

+

+ Provider settings are not available on the web platform. +

+
+
+
+
+ ) + } + return ( <> + +

{t('common:settings')}

+
+
+ +
+
+

+ {t('common:notAvailable')} +

+

+ Model provider settings are not available on the web platform. +

+
+
+
+
+ ) + } + return (
diff --git a/web-app/src/services/providers/web.ts b/web-app/src/services/providers/web.ts index c7b444388..30fe71366 100644 --- a/web-app/src/services/providers/web.ts +++ b/web-app/src/services/providers/web.ts @@ -9,43 +9,11 @@ import { ModelCapabilities } from '@/types/models' import { modelSettings } from '@/lib/predefined' import { ExtensionManager } from '@/lib/extension' import type { ProvidersService } from './types' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' export class WebProvidersService implements ProvidersService { async getProviders(): Promise { - const builtinProviders = predefinedProviders.map((provider) => { - let models = provider.models as Model[] - if (Object.keys(providerModels).includes(provider.provider)) { - const builtInModels = providerModels[ - provider.provider as unknown as keyof typeof providerModels - ].models as unknown as string[] - - if (Array.isArray(builtInModels)) - models = builtInModels.map((model) => { - const modelManifest = models.find((e) => e.id === model) - // TODO: Check chat_template for tool call support - const capabilities = [ - ModelCapabilities.COMPLETION, - ( - providerModels[ - provider.provider as unknown as keyof typeof providerModels - ].supportsToolCalls as unknown as string[] - ).includes(model) - ? ModelCapabilities.TOOLS - : undefined, - ].filter(Boolean) as string[] - return { - ...(modelManifest ?? { id: model, name: model }), - capabilities, - } as Model - }) - } - - return { - ...provider, - models, - } - }) - const runtimeProviders: ModelProvider[] = [] for (const [providerName, value] of EngineManager.instance().engines) { const models = (await value.list()) ?? [] @@ -105,6 +73,45 @@ export class WebProvidersService implements ProvidersService { runtimeProviders.push(provider) } + if (!PlatformFeatures[PlatformFeature.DEFAULT_PROVIDERS]) { + return runtimeProviders + } + + const builtinProviders = predefinedProviders.map((provider) => { + let models = provider.models as Model[] + if (Object.keys(providerModels).includes(provider.provider)) { + const builtInModels = providerModels[ + provider.provider as unknown as keyof typeof providerModels + ].models as unknown as string[] + + if (Array.isArray(builtInModels)) { + models = builtInModels.map((model) => { + const modelManifest = models.find((e) => e.id === model) + // TODO: Check chat_template for tool call support + const capabilities = [ + ModelCapabilities.COMPLETION, + ( + providerModels[ + provider.provider as unknown as keyof typeof providerModels + ].supportsToolCalls as unknown as string[] + ).includes(model) + ? ModelCapabilities.TOOLS + : undefined, + ].filter(Boolean) as string[] + return { + ...(modelManifest ?? { id: model, name: model }), + capabilities, + } as Model + }) + } + } + + return { + ...provider, + models, + } + }) + return runtimeProviders.concat(builtinProviders as ModelProvider[]) } @@ -132,7 +139,7 @@ export class WebProvidersService implements ProvidersService { if (!response.ok) { throw new Error( - `Failed to fetch models: ${response.status} ${response.statusText}` + `Cannot connect to ${provider.provider} at ${provider.base_url}. Please check that the service is running and accessible.` ) } @@ -163,14 +170,14 @@ export class WebProvidersService implements ProvidersService { } catch (error) { console.error('Error fetching models from provider:', error) - // Provide helpful error message - if (error instanceof Error && error.message.includes('fetch')) { - throw new Error( - `Cannot connect to ${provider.provider} at ${provider.base_url}. Please check that the service is running and accessible.` - ) + // Provide helpful error message for any connection errors + if (error instanceof Error && error.message.includes('Cannot connect')) { + throw error } - - throw error + + throw new Error( + `Cannot connect to ${provider.provider} at ${provider.base_url}. Please check that the service is running and accessible.` + ) } } diff --git a/web-app/src/test/setup.ts b/web-app/src/test/setup.ts index 2d3b53c0e..a542ffec9 100644 --- a/web-app/src/test/setup.ts +++ b/web-app/src/test/setup.ts @@ -18,6 +18,10 @@ vi.mock('@/lib/platform/const', () => ({ modelHub: true, systemIntegrations: true, httpsProxy: true, + defaultProviders: true, + analytics: true, + webAutoModelSelection: true, + modelProviderSettings: true, } })) @@ -28,11 +32,12 @@ const mockServiceHub = { setTheme: vi.fn(), toggleTheme: vi.fn(), }), - window: () => ({ + window: vi.fn().mockReturnValue({ minimize: vi.fn(), maximize: vi.fn(), close: vi.fn(), isMaximized: vi.fn().mockResolvedValue(false), + openLogsWindow: vi.fn().mockResolvedValue(undefined), }), events: () => ({ emit: vi.fn().mockResolvedValue(undefined), @@ -41,7 +46,7 @@ const mockServiceHub = { hardware: () => ({ getHardwareInfo: vi.fn().mockResolvedValue(null), getSystemUsage: vi.fn().mockResolvedValue(null), - getLlamacppDevices: vi.fn().mockResolvedValue([]), + getLlamacppDevices: vi.fn().mockResolvedValue([]), // cspell: disable-line setActiveGpus: vi.fn().mockResolvedValue(undefined), // Legacy methods for backward compatibility getGpuInfo: vi.fn().mockResolvedValue([]), @@ -52,6 +57,8 @@ const mockServiceHub = { getAppSettings: vi.fn().mockResolvedValue({}), updateAppSettings: vi.fn().mockResolvedValue(undefined), getSystemInfo: vi.fn().mockResolvedValue({}), + relocateJanDataFolder: vi.fn().mockResolvedValue(undefined), + getJanDataFolder: vi.fn().mockResolvedValue('/mock/jan/data'), }), analytic: () => ({ track: vi.fn(), @@ -104,6 +111,9 @@ const mockServiceHub = { deleteModel: vi.fn().mockResolvedValue(undefined), updateModel: vi.fn().mockResolvedValue(undefined), startModel: vi.fn().mockResolvedValue(undefined), + isModelSupported: vi.fn().mockResolvedValue('GREEN'), + checkMmprojExists: vi.fn().mockResolvedValue(true), // cspell: disable-line + stopAllModels: vi.fn().mockResolvedValue(undefined), }), assistants: () => ({ getAssistants: vi.fn().mockResolvedValue([]), @@ -117,15 +127,16 @@ const mockServiceHub = { save: vi.fn().mockResolvedValue('/path/to/file'), message: vi.fn().mockResolvedValue(undefined), }), - opener: () => ({ + opener: vi.fn().mockReturnValue({ open: vi.fn().mockResolvedValue(undefined), + revealItemInDir: vi.fn().mockResolvedValue(undefined), }), updater: () => ({ checkForUpdates: vi.fn().mockResolvedValue(null), installUpdate: vi.fn().mockResolvedValue(undefined), downloadAndInstallWithProgress: vi.fn().mockResolvedValue(undefined), }), - path: () => ({ + path: vi.fn().mockReturnValue({ sep: () => '/', join: vi.fn((...args) => args.join('/')), resolve: vi.fn((path) => path), @@ -137,7 +148,7 @@ const mockServiceHub = { stopCore: vi.fn().mockResolvedValue(undefined), getCoreStatus: vi.fn().mockResolvedValue('stopped'), }), - deeplink: () => ({ + deeplink: () => ({ // cspell: disable-line register: vi.fn().mockResolvedValue(undefined), handle: vi.fn().mockResolvedValue(undefined), getCurrent: vi.fn().mockResolvedValue(null), @@ -168,6 +179,27 @@ Object.defineProperty(window, 'matchMedia', { })), }) +// Mock globalThis.core.api for @janhq/core functions // cspell: disable-line +;(globalThis as Record).core = { + api: { + getJanDataFolderPath: vi.fn().mockResolvedValue('/mock/jan/data'), + openFileExplorer: vi.fn().mockResolvedValue(undefined), + joinPath: vi.fn((...paths: string[]) => paths.join('/')), + } +} + +// Mock globalThis.fs for @janhq/core fs functions // cspell: disable-line +;(globalThis as Record).fs = { + existsSync: vi.fn().mockResolvedValue(false), + readFile: vi.fn().mockResolvedValue(''), + writeFile: vi.fn().mockResolvedValue(undefined), + readdir: vi.fn().mockResolvedValue([]), + mkdir: vi.fn().mockResolvedValue(undefined), + unlink: vi.fn().mockResolvedValue(undefined), + rmdir: vi.fn().mockResolvedValue(undefined), +} + + // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup() diff --git a/web-app/vite.config.web.ts b/web-app/vite.config.web.ts index 48495623d..6f4e271be 100644 --- a/web-app/vite.config.web.ts +++ b/web-app/vite.config.web.ts @@ -64,4 +64,6 @@ export default defineConfig({ port: 3001, strictPort: true, }, + // Enable SPA mode - fallback to index.html for all routes + appType: 'spa', })