From 36b2f1076bd8509177cbeddfe1549dcc567bb221 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 3 Oct 2026 21:30:18 +0200 Subject: [PATCH] chore: bump plugin version to 0.1.25 --- package.json | 2 +- src/ui.tsx | 295 +++++---------------------------------------------- 2 files changed, 30 insertions(+), 267 deletions(-) diff --git a/package.json b/package.json index e301cde..c264484 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "figma-ai-plugin", - "version": "0.1.24", + "version": "0.1.25", "private": true, "dependencies": { "@create-figma-plugin/ui": "^4.0.3", diff --git a/src/ui.tsx b/src/ui.tsx index 8c61cc4..945d392 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -6,9 +6,6 @@ import { VerticalSpace, Stack, Button, - Textbox, - Dropdown, - Checkbox, Modal, IconButton, IconExpand24, @@ -19,10 +16,17 @@ import { } from '@create-figma-plugin/ui' import { emit, on } from '@create-figma-plugin/utilities' import { h, Fragment, Component } from 'preact' +import { lazy, Suspense } from 'preact/compat' import { useState, useEffect, useRef } from 'preact/hooks' import MenuItem from './MenuItem' -import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL, GONKAGATE_BASE_URL } from './settings' -import { APINEX_MODELS } from './apinex-models' +import { store, StoreContext, useScreen } from './store' + +// Lazy-loaded screens — their code is not executed until the screen is opened. +const MainScreen = lazy(() => import('./screens/MainScreen')) +const SettingsScreen = lazy(() => import('./screens/SettingsScreen')) + +// Max entries kept in the live request log. Prevents unbounded DOM growth. +const LOG_MAX = 200 const MIN_WINDOW_SIZE = { width: 320, height: 200 } // No static max: "maximize" targets the whole viewport (maxViewport from main), @@ -30,7 +34,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.24' +const PLUGIN_VERSION = '0.1.25' const fieldStyle = { display: 'flex', @@ -97,269 +101,28 @@ 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 [savedNote, setSavedNote] = useState(false) - const fileInputRef = useRef(null) - const [modelsError, setModelsError] = useState(null) - const settingsRef = useRef(settings) - settingsRef.current = settings - - const providers = Array.isArray(settings.providers) ? settings.providers : [] - 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, providerId?: string } }) { - setFetching(false) - logEvent('MODELS_RESULT · ok=' + payload.ok + ' · status=' + (payload.status != null ? payload.status : '?')) - if (payload.requestEcho != null) { - logEvent(' request reached main · baseUrl=' + payload.requestEcho.baseUrl + ' · keyLen=' + payload.requestEcho.keyLen) - } - if (!payload.ok) { - const msg = payload.error != null - ? (typeof payload.error === 'string' ? payload.error : JSON.stringify(payload.error)) - : 'Failed to load models' - logEvent(' error: ' + msg) - if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000)) - 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 - 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 patchProvider (id: string, p: Partial) { - const updatedProviders = providers.map(function (pr) { - return pr.id === id ? { ...pr, ...p } : pr - }) - setSettings({ ...settings, providers: updatedProviders }) - } - - function setActive (id: string) { - setSettings({ ...settings, activeProviderId: id }) - logEvent('Active provider → ' + (providers.find(function (p) { return p.id === id }) || { name: id }).name) - } - - function setPluginToken (v: string) { - setSettings({ ...settings, pluginToken: v }) - } - - function fetchModels (provider: ProviderConfig) { - setFetching(true) - setModelsError(null) - 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, pluginToken: settings.pluginToken }) - } - - function save () { - saveSettings(settings).then(function () { - setSavedNote(true) - logEvent('Settings saved (manual)') - setTimeout(function () { setSavedNote(false) }, 1500) - }) - } - - function exportSettings () { - const json = settingsToJSON(settings) - const blob = new Blob([json], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = 'figma-ai-plugin-settings.json' - a.click() - URL.revokeObjectURL(url) - } - - function onImportFile (e: Event) { - const input = e.target as HTMLInputElement - const file = input.files != null ? input.files[0] : null - if (file == null) return - const reader = new FileReader() - reader.onload = function () { - try { - const s = settingsFromJSON(String(reader.result)) - setSettings(s) - saveSettings(s) - } catch (err) { - // TODO: surface parse error to the user - } - } - reader.readAsText(file) - input.value = '' - } - - if (providers.length === 0) { - return No provider configured. - } - - return ( -
-
- Plugin token (CORS proxy) - - Used to authenticate requests to the CORS proxy (ai-assistant-fig.dynv6.net). Pre-filled with the dev token; edit if you rotate it. - - -
- - - -
- Active provider - -
- - - - {providers.map(function (provider) { - return ( - ) { patchProvider(provider.id, p) }} - onFetch={function () { fetchModels(provider) }} - fetching={fetching} - modelsError={modelsError} - /> - ) - })} - -
- - {savedNote && Saved} -
- - - -
- Backup & restore -
- - - -
-
-
- ) -} - function Plugin () { + const screen = useScreen() const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE) - const [settings, setSettings] = useState(defaultSettings()) - const [loaded, setLoaded] = useState(false) const [log, setLog] = useState([]) const [copied, setCopied] = useState(false) - const [fetching, setFetching] = useState(false) + const [moreOpen, setMoreOpen] = useState(false) function logEvent (msg: string) { const t = new Date().toISOString().slice(11, 23) - setLog(function (prev) { return prev.concat('[' + t + '] ' + msg) }) + setLog(function (prev) { + const next = prev.concat('[' + t + '] ' + msg) + return next.length > LOG_MAX ? next.slice(next.length - LOG_MAX) : next + }) } function copyLog () { copyText(log.join('\n')) setCopied(true) setTimeout(function () { setCopied(false) }, 1500) } + // Bootstrap store (loads persisted settings from main thread once). useEffect(function () { - loadSettings() - .then(function (s) { setSettings(s) }) - .catch(function () {}) - .finally(function () { setLoaded(true) }) + store.init() }, []) - - // Auto-persist settings (incl. API key) on every change — no need to click Save. - // Gated on `loaded` so we don't overwrite stored settings with defaults before load completes. - useEffect(function () { - if (!loaded) return - const t = setTimeout(function () { saveSettings(settings).then(function () { logEvent('Settings saved') }) }, 400) - return function () { clearTimeout(t) } - }, [settings, loaded]) - useEffect(function () { try { logEvent('Plugin loaded') } catch (e) {} }, []) const resizeRef = useRef({ active: false, dir: '', startX: 0, startY: 0, startW: 0, startH: 0, fixedAxis: 'none' as 'none' | 'x' | 'y' }) // Current dock position (for "window is edge-docked" detection in UI). Center and // corners are NOT written here — they are not considered docked (window just lies there). @@ -616,7 +379,6 @@ function Plugin () { }) } - const [moreOpen, setMoreOpen] = useState(false) const [isMaxToggled, setIsMaxToggled] = useState(false) // Resize-handle visibility is state-driven (not CSS :hover/:has) for Figma webview // reliability. Handles appear only after the cursor has rested in the window for 500ms @@ -650,13 +412,14 @@ function Plugin () { const isMaximized = isMaxToggled return ( +
{/* Header — navigation bar */}
- Settings + {screen === 'main' ? 'Main' : 'Settings'}