diff --git a/package.json b/package.json index 705808d..688dbfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "figma-ai-plugin", - "version": "0.1.20", + "version": "0.1.21", "private": true, "dependencies": { "@create-figma-plugin/ui": "^4.0.3", diff --git a/src/main.ts b/src/main.ts index 234b7f8..265bf13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -514,7 +514,7 @@ export default function () { // UI asks to list models for a provider. Network must run here (main thread) — // the plugin UI iframe is CSP-sandboxed and cannot fetch external URLs. on('FETCH_MODELS', async function (payload: any) { - const echo = { baseUrl: payload.baseUrl, keyLen: payload.apiKey != null ? payload.apiKey.length : 0 } + const echo = { baseUrl: payload.baseUrl, keyLen: payload.apiKey != null ? payload.apiKey.length : 0, providerId: payload.providerId != null ? payload.providerId : null } try { if (typeof fetch !== 'function') { emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [], status: 0, requestEcho: echo }) diff --git a/src/settings.ts b/src/settings.ts index 8d16e04..4aa71a9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -8,7 +8,7 @@ import { emit, on } from '@create-figma-plugin/utilities' import { APINEX_MODELS } from './apinex-models' -export type ProviderType = 'apinex' +export type ProviderType = 'apinex' | 'gonkagate' export interface ProviderConfig { id: string @@ -28,15 +28,18 @@ export interface Settings { } export const APINEX_BASE_URL = 'https://api.apinex.bond/v1' +export const GONKAGATE_BASE_URL = 'https://api.gonkagate.com/v1' const SETTINGS_KEY = 'settings' export function defaultSettings (): Settings { - const p = createApinexProvider() + const a = createApinexProvider() + const g = createGonkagateProvider() + // First provider (APInex) is the active one by default. return { version: 1, auth: { mode: 'local' }, - providers: [p], - activeProviderId: p.id + providers: [a, g], + activeProviderId: a.id } } @@ -56,6 +59,18 @@ export function createApinexProvider (): ProviderConfig { } } +export function createGonkagateProvider (): ProviderConfig { + return { + id: genId(), + name: 'GonkaGate', + type: 'gonkagate', + baseUrl: GONKAGATE_BASE_URL, + apiKey: '', + defaultModel: '', + models: [] + } +} + // Request main thread to load persisted settings; resolves with merged Settings. export function loadSettings (): Promise { return new Promise(function (resolve) { diff --git a/src/ui.tsx b/src/ui.tsx index ac4c051..87abbc4 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -21,7 +21,7 @@ import { emit, on } from '@create-figma-plugin/utilities' import { h, Fragment, Component } from 'preact' import { useState, useEffect, useRef } from 'preact/hooks' import MenuItem from './MenuItem' -import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL } from './settings' +import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL, GONKAGATE_BASE_URL } from './settings' import { APINEX_MODELS } from './apinex-models' const MIN_WINDOW_SIZE = { width: 320, height: 200 } @@ -30,7 +30,7 @@ const MIN_WINDOW_SIZE = { width: 320, height: 200 } // very high — actual clamp is done by main via clampWindowSize (bounds*zoom). // Full plugin version shown in the footer status bar. Keep in sync with package.json "version". -const PLUGIN_VERSION = '0.1.20' +const PLUGIN_VERSION = '0.1.21' const fieldStyle = { display: 'flex', @@ -97,21 +97,79 @@ class ErrorBoundary extends Component<{ children: any }, { error: Error | null } } } +function ProviderCard (props: { + provider: ProviderConfig + isActive: boolean + onActivate: () => void + onPatch: (p: Partial) => void + onFetch: () => void + fetching: boolean + modelsError: string | null +}) { + const { provider, isActive, onActivate, onPatch, onFetch, fetching, modelsError } = props + const [showKey, setShowKey] = useState(false) + const placeholderKey = provider.type === 'gonkagate' ? 'gp-…' : 'sk-apx…' + const basePlaceholder = provider.type === 'gonkagate' ? GONKAGATE_BASE_URL : APINEX_BASE_URL + const modelOptions = (provider.models && provider.models.length > 0) ? provider.models : APINEX_MODELS + + return ( +
+
+ Provider · {provider.type} + +
+ + + Show key + + +
+ ) +} + function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void, logEvent: (msg: string) => void, fetching: boolean, setFetching: (v: boolean) => void }) { const { settings, setSettings, logEvent, fetching, setFetching } = props - const [showKey, setShowKey] = useState(false) const [savedNote, setSavedNote] = useState(false) const fileInputRef = useRef(null) const [modelsError, setModelsError] = useState(null) const settingsRef = useRef(settings) settingsRef.current = settings - // Static single provider (APInex). Always edit providers[0]. const providers = Array.isArray(settings.providers) ? settings.providers : [] - const provider = providers.length > 0 ? providers[0] : null + const activeId = settings.activeProviderId + const activeProvider = providers.find(function (p) { return p.id === activeId }) || (providers.length > 0 ? providers[0] : null) useEffect(function () { - const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number } }) { + const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number, providerId?: string } }) { setFetching(false) logEvent('MODELS_RESULT · ok=' + payload.ok + ' · status=' + (payload.status != null ? payload.status : '?')) if (payload.requestEcho != null) { @@ -123,34 +181,41 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) : 'Failed to load models' logEvent(' error: ' + msg) if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000)) - setModelsError('Live fetch unavailable (sandbox has no internet) — using bundled list') + setModelsError('Live fetch failed — using bundled list') return } logEvent(' models parsed=' + (payload.models ? payload.models.length : 0)) if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000)) setModelsError(null) const prev = settingsRef.current - if (prev.providers[0] == null) return - const updated = { ...prev.providers[0], models: payload.models } - setSettings({ ...prev, providers: [updated] }) + const targetId = payload.requestEcho != null && payload.requestEcho.providerId != null + ? payload.requestEcho.providerId + : (prev.activeProviderId || (prev.providers[0] != null ? prev.providers[0].id : null)) + const updatedProviders = prev.providers.map(function (p) { + return p.id === targetId ? { ...p, models: payload.models } : p + }) + setSettings({ ...prev, providers: updatedProviders }) }) return dispose }, []) - function patch (p: Partial) { - if (provider == null) return - setSettings({ - ...settings, - providers: [{ ...provider, ...p }] + function patchProvider (id: string, p: Partial) { + const updatedProviders = providers.map(function (pr) { + return pr.id === id ? { ...pr, ...p } : pr }) + setSettings({ ...settings, providers: updatedProviders }) } - function fetchModels () { - if (provider == null) return + function setActive (id: string) { + setSettings({ ...settings, activeProviderId: id }) + logEvent('Active provider → ' + (providers.find(function (p) { return p.id === id }) || { name: id }).name) + } + + function fetchModels (provider: ProviderConfig) { setFetching(true) setModelsError(null) - logEvent('FETCH clicked · baseUrl=' + provider.baseUrl + ' · key=' + (provider.apiKey.trim() !== '' ? 'PRESENT(' + provider.apiKey.length + ' chars)' : 'EMPTY')) - emit('FETCH_MODELS', { baseUrl: provider.baseUrl, ['api' + 'Key']: provider.apiKey }) + logEvent('FETCH clicked · provider=' + provider.name + ' · baseUrl=' + provider.baseUrl + ' · key=' + (provider.apiKey.trim() !== '' ? 'PRESENT(' + provider.apiKey.length + ' chars)' : 'EMPTY')) + emit('FETCH_MODELS', { providerId: provider.id, baseUrl: provider.baseUrl, ['api' + 'Key']: provider.apiKey }) } function save () { @@ -190,49 +255,39 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) input.value = '' } - if (provider == null) { + if (providers.length === 0) { return No provider configured. } - const modelOptions = (provider.models && provider.models.length > 0) ? provider.models : APINEX_MODELS - return (
- Provider · {provider.type} - - - Show key - - + Active provider +
+ + + {providers.map(function (provider) { + return ( + ) { patchProvider(provider.id, p) }} + onFetch={function () { fetchModels(provider) }} + fetching={fetching} + modelsError={modelsError} + /> + ) + })} +
{savedNote && Saved}