feat: add GonkaGate provider (static form + active-provider selector); proxy allowlist gains api.gonkagate.com; bump 0.1.21
This commit is contained in:
1 parent
b42eae810a
commit
c4f7e853b1
4 files changed
+130
-60
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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 })
|
||||
|
||||
+19
-4
@@ -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<Settings> {
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
+109
-54
@@ -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<ProviderConfig>) => 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 (
|
||||
<section style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-8)', border: isActive ? '1px solid var(--figma-color-border)' : '1px solid transparent', borderRadius: 8, padding: isActive ? 'var(--space-8)' : 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Provider · {provider.type}</Text>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
|
||||
<span style={labelStyle}>Active</span>
|
||||
<Checkbox value={isActive} onValueChange={function (v: boolean) { if (v) onActivate() }}>Use</Checkbox>
|
||||
</label>
|
||||
</div>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Name</span>
|
||||
<Textbox value={provider.name} placeholder={provider.type === 'gonkagate' ? 'GonkaGate' : 'APInex'} onValueInput={function (v: string) { onPatch({ name: v }) }} />
|
||||
</label>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>API key</span>
|
||||
<Textbox password={!showKey} value={provider.apiKey} placeholder={placeholderKey} onValueInput={function (v: string) { onPatch({ apiKey: v }) }} />
|
||||
</label>
|
||||
<Checkbox value={showKey} onValueChange={setShowKey}>Show key</Checkbox>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Base URL</span>
|
||||
<Textbox value={provider.baseUrl} placeholder={basePlaceholder} onValueInput={function (v: string) { onPatch({ baseUrl: v }) }} />
|
||||
</label>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Default model</span>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center' }}>
|
||||
<Dropdown
|
||||
placeholder='Select model'
|
||||
value={provider.defaultModel === '' ? null : provider.defaultModel}
|
||||
onValueChange={function (id: string) { onPatch({ defaultModel: id }) }}
|
||||
options={modelOptions.map(function (m) { return { value: m, text: m } })}
|
||||
/>
|
||||
<Button onClick={onFetch} disabled={provider.apiKey.trim() === '' || fetching}>
|
||||
{fetching ? 'Fetching…' : 'Fetch'}
|
||||
</Button>
|
||||
{fetching && <Muted>request is running…</Muted>}
|
||||
</div>
|
||||
{modelsError != null && (
|
||||
<Muted style={{ color: 'var(--figma-color-text-danger)' }}>{modelsError}</Muted>
|
||||
)}
|
||||
</label>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
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<HTMLInputElement>(null)
|
||||
const [modelsError, setModelsError] = useState<string | null>(null)
|
||||
const settingsRef = useRef<Settings>(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<ProviderConfig>) {
|
||||
if (provider == null) return
|
||||
setSettings({
|
||||
...settings,
|
||||
providers: [{ ...provider, ...p }]
|
||||
function patchProvider (id: string, p: Partial<ProviderConfig>) {
|
||||
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 <Muted>No provider configured.</Muted>
|
||||
}
|
||||
|
||||
const modelOptions = (provider.models && provider.models.length > 0) ? provider.models : APINEX_MODELS
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-medium)' }}>
|
||||
<section style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-8)' }}>
|
||||
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Provider · {provider.type}</Text>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Name</span>
|
||||
<Textbox value={provider.name} placeholder="APInex" onValueInput={function (v: string) { patch({ name: v }) }} />
|
||||
</label>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>API key</span>
|
||||
<Textbox password={!showKey} value={provider.apiKey} placeholder="sk-apx…" onValueInput={function (v: string) { patch({ apiKey: v }) }} />
|
||||
</label>
|
||||
<Checkbox value={showKey} onValueChange={setShowKey}>Show key</Checkbox>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Base URL</span>
|
||||
<Textbox value={provider.baseUrl} placeholder={APINEX_BASE_URL} onValueInput={function (v: string) { patch({ baseUrl: v }) }} />
|
||||
</label>
|
||||
<label style={fieldStyle}>
|
||||
<span style={labelStyle}>Default model</span>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-4)', alignItems: 'center' }}>
|
||||
<Dropdown
|
||||
placeholder='Select model'
|
||||
value={provider.defaultModel === '' ? null : provider.defaultModel}
|
||||
onValueChange={function (id: string) { patch({ defaultModel: id }) }}
|
||||
options={modelOptions.map(function (m) { return { value: m, text: m } })}
|
||||
/>
|
||||
<Button onClick={fetchModels} disabled={provider.apiKey.trim() === '' || fetching}>
|
||||
{fetching ? 'Fetching…' : 'Fetch'}
|
||||
</Button>
|
||||
{fetching && <Muted>request is running…</Muted>}
|
||||
</div>
|
||||
{modelsError != null && (
|
||||
<Muted style={{ color: 'var(--figma-color-text-danger)' }}>{modelsError}</Muted>
|
||||
)}
|
||||
</label>
|
||||
<Text style={{ fontWeight: 600, fontSize: '12px' }}>Active provider</Text>
|
||||
<Dropdown
|
||||
placeholder='Select active provider'
|
||||
value={activeProvider != null ? activeProvider.id : null}
|
||||
onValueChange={function (id: string) { setActive(id) }}
|
||||
options={providers.map(function (p) { return { value: p.id, text: p.name + ' (' + p.type + ')' } })}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{providers.map(function (provider) {
|
||||
return (
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
isActive={provider.id === activeId}
|
||||
onActivate={function () { setActive(provider.id) }}
|
||||
onPatch={function (p: Partial<ProviderConfig>) { patchProvider(provider.id, p) }}
|
||||
onFetch={function () { fetchModels(provider) }}
|
||||
fetching={fetching}
|
||||
modelsError={modelsError}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--space-8)', alignItems: 'center' }}>
|
||||
<Button onClick={save}>Save</Button>
|
||||
{savedNote && <Muted>Saved</Muted>}
|
||||
|
||||
Reference in new issue
Block a user