diff --git a/package.json b/package.json index bfdcb3f..bf7c1dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "figma-ai-plugin", - "version": "0.1.6", + "version": "0.1.7", "private": true, "dependencies": { "@create-figma-plugin/ui": "^4.0.3", diff --git a/src/main.ts b/src/main.ts index a1d7649..1c4e144 100644 --- a/src/main.ts +++ b/src/main.ts @@ -505,37 +505,44 @@ 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 } try { if (typeof fetch !== 'function') { - emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [] }) + emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [], status: 0, requestEcho: echo }) return } const base = payload.baseUrl.replace(/\/+$/, '') const res = await fetch(base + '/models', { headers: { Authorization: 'Bearer ' + payload.apiKey } }) + const raw = await res.text() if (!res.ok) { let msg = 'HTTP ' + res.status try { - const body = JSON.parse(await res.text()) + const body = JSON.parse(raw) if (body != null && body.error != null) { msg = typeof body.error === 'string' ? body.error : (body.error.message || JSON.stringify(body.error)) } } catch (e) { /* keep HTTP status message */ } - emit('MODELS_RESULT', { ok: false, error: msg, models: [] }) + emit('MODELS_RESULT', { ok: false, error: msg, models: [], status: res.status, raw: raw, requestEcho: echo }) return } - const json: any = await res.json() let models: string[] = [] - if (Array.isArray(json.data)) { - models = json.data.map((m: any) => m.id).filter((x: any) => typeof x === 'string' && x.length > 0) - } else if (Array.isArray(json.models)) { - models = json.models.map((m: any) => m.id || m).filter((x: any) => typeof x === 'string') + try { + const json: any = JSON.parse(raw) + if (Array.isArray(json.data)) { + models = json.data.map((m: any) => m.id).filter((x: any) => typeof x === 'string' && x.length > 0) + } else if (Array.isArray(json.models)) { + models = json.models.map((m: any) => m.id || m).filter((x: any) => typeof x === 'string') + } + } catch (e) { + emit('MODELS_RESULT', { ok: false, error: 'Failed to parse models JSON', models: [], status: res.status, raw: raw, requestEcho: echo }) + return } - emit('MODELS_RESULT', { ok: true, models }) + emit('MODELS_RESULT', { ok: true, models: models, status: res.status, raw: raw, requestEcho: echo }) } catch (e) { const msg = e instanceof Error ? e.message : (e != null ? String(e) : 'Unknown error') - emit('MODELS_RESULT', { ok: false, error: msg, models: [] }) + emit('MODELS_RESULT', { ok: false, error: msg, models: [], status: 0, requestEcho: echo }) } }) diff --git a/src/ui.tsx b/src/ui.tsx index ad8d665..f19def1 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -29,7 +29,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.6' +const PLUGIN_VERSION = '0.1.7' const fieldStyle = { display: 'flex', @@ -55,6 +55,27 @@ function Separator () { ) } +function copyText (text: string): void { + try { + if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text) + return + } + } catch (e) { /* fall through to legacy copy */ } + try { + const ta = document.createElement('textarea') + ta.value = text + ta.style.position = 'fixed' + ta.style.top = '-1000px' + ta.style.opacity = '0' + document.body.appendChild(ta) + ta.focus() + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) + } catch (e) { /* ignore */ } +} + class ErrorBoundary extends Component<{ children: any }, { error: Error | null }> { constructor (props: { children: any }) { super(props) @@ -75,8 +96,8 @@ class ErrorBoundary extends Component<{ children: any }, { error: Error | null } } } -function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void }) { - const { settings, setSettings } = props +function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void, logEvent: (msg: string) => void }) { + const { settings, setSettings, logEvent } = props const [showKey, setShowKey] = useState(false) const [savedNote, setSavedNote] = useState(false) const fileInputRef = useRef(null) @@ -89,14 +110,22 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) const provider = providers.length > 0 ? providers[0] : null useEffect(function () { - const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string }) { + const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number } }) { + 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(msg) 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 @@ -117,7 +146,8 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) function fetchModels () { if (provider == null) return setModelsError(null) - emit('FETCH_MODELS', { baseUrl: provider.baseUrl, apiKey: provider.apiKey }) + 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 }) } function save () { @@ -217,7 +247,19 @@ function Plugin () { 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) + function logEvent (msg: string) { + const t = new Date().toISOString().slice(11, 23) + setLog(function (prev) { return prev.concat('[' + t + '] ' + msg) }) + } + function copyLog () { + copyText(log.join('\n')) + setCopied(true) + setTimeout(function () { setCopied(false) }, 1500) + } useEffect(function () { + logEvent('Plugin loaded · figma.version=' + (figma as any).version) loadSettings() .then(function (s) { setSettings(s) }) .catch(function () {}) @@ -521,12 +563,7 @@ function Plugin () {
Settings