fix: real API key now saved/sent (was mangled by apiKey: literal); add Copy Log button in header with actual API response (status+raw) and request echo
This commit is contained in:
1 parent
d62c453ff5
commit
427155f93f
3 files changed
+67
-23
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
+17
-10
@@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+49
-12
@@ -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<HTMLInputElement>(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<Settings>(defaultSettings())
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
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 () {
|
||||
<header style={{ display: 'flex', alignItems: 'center', flex: '0 0 auto', padding: 'var(--space-8)' }}>
|
||||
<Text style={{ fontSize: '13px', fontWeight: 600 }}>Settings</Text>
|
||||
<nav style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', marginLeft: 'auto' }}>
|
||||
<IconButton title="Stub action" onClick={function () {}}>
|
||||
<IconWand24 />
|
||||
</IconButton>
|
||||
<IconButton title="Stub action" onClick={function () {}}>
|
||||
<IconComponent24 />
|
||||
</IconButton>
|
||||
<Button onClick={copyLog}>{copied ? 'Copied' : 'Copy Log'}</Button>
|
||||
<IconButton
|
||||
title={isMaximized ? 'Restore window' : 'Maximize window'}
|
||||
onClick={toggleWindowSize}
|
||||
@@ -542,7 +579,7 @@ function Plugin () {
|
||||
{/* Body — semantic <main>, stretches via flex child of the column */}
|
||||
<main style={{ flex: '1 1 auto', overflow: 'auto', padding: 'var(--space-8)' }}>
|
||||
{loaded
|
||||
? <SettingsPanel settings={settings} setSettings={setSettings} />
|
||||
? <SettingsPanel settings={settings} setSettings={setSettings} logEvent={logEvent} />
|
||||
: <Muted>Loading…</Muted>}
|
||||
</main>
|
||||
{/* Footer — status bar */}
|
||||
|
||||
Reference in new issue
Block a user