From 8cb6d008fcb645d0c00bef710f2d0ab4a6187542 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 19 May 2026 13:59:31 +0300 Subject: [PATCH] fix(ai): allow custom OpenRouter models --- CHANGELOG.md | 7 +- lint/plugin.js | 1 + packages/core/src/constants.ts | 1 + src/app/ai/chat/model.ts | 15 +- src/app/ai/chat/provider-models.ts | 74 ++++++++++ src/app/ai/chat/transports.ts | 5 +- src/app/cache/index.ts | 129 ++++++++++++++++++ src/app/editor/fonts/cache.ts | 108 +++++---------- src/components/AppMenu.vue | 9 +- src/components/AssetsPanel.vue | 9 +- src/components/CanvasMenu.vue | 17 ++- src/components/ChatPanel.vue | 19 +-- src/components/CodePanel.vue | 26 ++-- src/components/CollabPanel/ConnectedRoom.vue | 9 +- src/components/CollabPanel/JoinRoomPrompt.vue | 8 +- .../CollabPanel/ShareOrJoinRoom.vue | 16 +-- src/components/Toolbar/ToolFlyout.vue | 5 +- src/components/ZoomDropdown.vue | 11 +- src/components/chat/ChatInput.vue | 18 ++- src/components/chat/ProviderModelSelect.vue | 5 +- .../chat/ProviderSettings/ApiKeySection.vue | 47 ++----- .../chat/ProviderSettings/ApiTypeSection.vue | 9 +- .../CustomEndpointSection.vue | 45 ++++-- .../ProviderSettings/MaxTokensSection.vue | 13 +- .../ProviderSettingsField.vue | 26 ++++ .../ProviderSettingsInput.vue | 39 ++++++ .../ProviderSettingsKeyField.vue | 50 +++++++ .../ProviderSettings/ProviderSettingsLink.vue | 12 ++ .../StockPhotoKeysSection.vue | 91 ++++-------- src/components/chat/ProviderSetup.vue | 36 +++-- .../properties/AppearanceSection.vue | 7 +- src/components/properties/StrokeSection.vue | 6 +- src/components/ui/AppBadge.vue | 18 +++ src/components/ui/AppComboboxInput.vue | 120 ++++++++++++++++ src/components/ui/AppInput.vue | 60 ++++++++ src/components/ui/AppShortcutText.vue | 16 +++ src/components/ui/AppTextButton.vue | 32 +++++ tests/e2e/chat/panel.spec.ts | 24 +++- tests/engine/app/ai/model.test.ts | 48 +++++++ tests/engine/app/cache.test.ts | 59 ++++++++ 40 files changed, 952 insertions(+), 298 deletions(-) create mode 100644 src/app/ai/chat/provider-models.ts create mode 100644 src/app/cache/index.ts create mode 100644 src/components/chat/ProviderSettings/ProviderSettingsField.vue create mode 100644 src/components/chat/ProviderSettings/ProviderSettingsInput.vue create mode 100644 src/components/chat/ProviderSettings/ProviderSettingsKeyField.vue create mode 100644 src/components/chat/ProviderSettings/ProviderSettingsLink.vue create mode 100644 src/components/ui/AppBadge.vue create mode 100644 src/components/ui/AppComboboxInput.vue create mode 100644 src/components/ui/AppInput.vue create mode 100644 src/components/ui/AppShortcutText.vue create mode 100644 src/components/ui/AppTextButton.vue create mode 100644 tests/engine/app/ai/model.test.ts create mode 100644 tests/engine/app/cache.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2317021ed..f7dddf81d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Allow OpenRouter users to enter any model ID from provider settings with cached autocomplete suggestions for tool-capable models, while keeping the curated dropdown as the default when no custom model is set. + ## 0.12.1 — 2026-05-19 ### Fixes @@ -357,14 +361,12 @@ - Fix text typography lost on Figma clipboard import — preserve fontFamily, fontWeight, fontSize, lineHeight - Fix `copyFill` missing `gradientTransform` and `imageTransform` — gradient fills now round-trip correctly - ### Performance - Event-driven rendering and component sync — `SceneGraph` emits typed events on mutations; `requestRender()` calls reduced from 94 to 22, component instance sync uses microtask batching with deduplication - Replace `structuredClone` with typed copy helpers for fills, strokes, effects, and style runs (~24× faster in hot paths) - Filter .fig unzip to only decompress canvas and image entries, skipping metadata cruft - ### Improvements - Padding on a frame auto-enables vertical auto-layout @@ -493,7 +495,6 @@ - Apple code signing and notarization for macOS builds - Git LFS storage moved from GitHub to Cloudflare R2 - ### Fixes - Fix Figma clipboard paste: extract shared kiwi→SceneNode conversion, fixing broken auto-layout, missing gradient/image fills, effects, style runs, and text properties diff --git a/lint/plugin.js b/lint/plugin.js index 834d38b26..4feb50f31 100644 --- a/lint/plugin.js +++ b/lint/plugin.js @@ -871,6 +871,7 @@ const noDirectStorageAccess = { const file = normalizedFilename(context) const allowedFiles = [ '/src/app/ai/chat/storage.ts', + '/src/app/cache/index.ts', '/src/app/shell/layout-storage.ts', '/packages/vue/src/i18n/locale.ts' ] diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 97b90bb72..ee83891cb 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -208,6 +208,7 @@ export const AI_PROVIDERS: AIProviderDef[] = [ keyPlaceholder: 'sk-or-…', keyURL: 'https://openrouter.ai/keys', defaultModel: 'anthropic/claude-sonnet-4.6', + supportsCustomModel: true, models: [ { id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', tag: 'Best for design' }, { id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', tag: 'Smartest' }, diff --git a/src/app/ai/chat/model.ts b/src/app/ai/chat/model.ts index 02a784da1..caa5cf28d 100644 --- a/src/app/ai/chat/model.ts +++ b/src/app/ai/chat/model.ts @@ -16,10 +16,19 @@ export type ModelConfig = { customAPIType: 'completions' | 'responses' } +export function resolveLanguageModelID( + config: Pick +) { + const customModelID = config.customModelID.trim() + if (config.providerID === 'openrouter') return customModelID || config.modelID + if (config.providerID === 'openai-compatible' || config.providerID === 'anthropic-compatible') { + return customModelID + } + return config.modelID +} + export function createLanguageModel(config: ModelConfig): LanguageModel { - const needsCustomModel = - config.providerID === 'openai-compatible' || config.providerID === 'anthropic-compatible' - const effectiveModelID = needsCustomModel ? config.customModelID : config.modelID + const effectiveModelID = resolveLanguageModelID(config) switch (config.providerID) { case 'openrouter': { diff --git a/src/app/ai/chat/provider-models.ts b/src/app/ai/chat/provider-models.ts new file mode 100644 index 000000000..2e84177fe --- /dev/null +++ b/src/app/ai/chat/provider-models.ts @@ -0,0 +1,74 @@ +import { AI_PROVIDERS } from '@open-pencil/core/constants' +import type { AIProviderID, ModelOption } from '@open-pencil/core/constants' + +import { readCacheJson, writeCacheJson } from '@/app/cache' + +type OpenRouterModel = { + id?: unknown + name?: unknown + supported_parameters?: unknown +} + +type OpenRouterModelsResponse = { + data?: OpenRouterModel[] +} + +const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models' +const OPENROUTER_MODELS_CACHE_KEY = 'openrouter/models' +const OPENROUTER_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1000 +function curatedProviderModels(providerID: AIProviderID) { + return AI_PROVIDERS.find((provider) => provider.id === providerID)?.models ?? [] +} + +const curatedOpenRouterModels = curatedProviderModels('openrouter') + +let modelsPromise: Promise | null = null + +function isToolCapableOpenRouterModel(model: OpenRouterModel) { + return Array.isArray(model.supported_parameters) && model.supported_parameters.includes('tools') +} + +export function normalizeOpenRouterModel(model: OpenRouterModel): ModelOption | null { + if (!isToolCapableOpenRouterModel(model)) return null + if (typeof model.id !== 'string' || !model.id) return null + return { + id: model.id, + name: typeof model.name === 'string' && model.name ? model.name : model.id + } +} + +async function fetchOpenRouterModels(fetcher: typeof fetch): Promise { + const response = await fetcher(OPENROUTER_MODELS_URL) + if (!response.ok) throw new Error(`OpenRouter models request failed: ${response.status}`) + const json = (await response.json()) as OpenRouterModelsResponse + return json.data?.map(normalizeOpenRouterModel).filter((model) => model !== null) ?? [] +} + +async function listOpenRouterModels(fetcher: typeof fetch = fetch): Promise { + modelsPromise ??= (async () => { + const cached = await readCacheJson( + OPENROUTER_MODELS_CACHE_KEY, + OPENROUTER_MODELS_CACHE_TTL_MS + ) + if (cached?.length) return cached + + try { + const models = await fetchOpenRouterModels(fetcher) + if (!models.length) return curatedOpenRouterModels + await writeCacheJson(OPENROUTER_MODELS_CACHE_KEY, models) + return models + } catch { + return curatedOpenRouterModels + } + })() + + return modelsPromise +} + +export async function listProviderModels( + providerID: AIProviderID, + fetcher: typeof fetch = fetch +): Promise { + if (providerID === 'openrouter') return listOpenRouterModels(fetcher) + return curatedProviderModels(providerID) +} diff --git a/src/app/ai/chat/transports.ts b/src/app/ai/chat/transports.ts index d47b3d450..f47480866 100644 --- a/src/app/ai/chat/transports.ts +++ b/src/app/ai/chat/transports.ts @@ -6,7 +6,7 @@ import type { ComputedRef, Ref } from 'vue' import { ACP_AGENTS } from '@open-pencil/core/constants' import type { ACPAgentID, AIProviderID } from '@open-pencil/core/constants' -import { createLanguageModel } from '@/app/ai/chat/model' +import { createLanguageModel, resolveLanguageModelID } from '@/app/ai/chat/model' import SYSTEM_PROMPT from '@/app/ai/chat/system-prompt.md?raw' import { MAX_AGENT_STEPS, createAITools, recordStepUsage, resetRunSteps } from '@/app/ai/tools' import type { getActiveEditorStore } from '@/app/editor/active-store' @@ -70,7 +70,8 @@ export function createToolLoopTransport({ maxOutputTokens }: ToolLoopTransportOptions) { const tools = createAITools(store) - const cacheProviderOptions = supportsAnthropicCaching(providerID, modelID) + const effectiveModelID = resolveLanguageModelID({ providerID, modelID, customModelID }) + const cacheProviderOptions = supportsAnthropicCaching(providerID, effectiveModelID) ? ANTHROPIC_CACHE_CONTROL : undefined diff --git a/src/app/cache/index.ts b/src/app/cache/index.ts new file mode 100644 index 000000000..2b75bf30c --- /dev/null +++ b/src/app/cache/index.ts @@ -0,0 +1,129 @@ +import { IS_BROWSER, IS_TAURI } from '@open-pencil/core/constants' + +const APP_CACHE_DIR = 'cache/v1' +const STORAGE_PREFIX = 'open-pencil:cache:v1:' + +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +function isTauriRuntime() { + return IS_TAURI +} + +function isStorageAvailable() { + return IS_BROWSER || 'window' in globalThis +} + +function cachePath(key: string) { + return `${APP_CACHE_DIR}/${key.split('/').map(encodeURIComponent).join('/')}` +} + +function storageKey(key: string) { + return `${STORAGE_PREFIX}${key}` +} + +export async function readCacheText(key: string): Promise { + if (isTauriRuntime()) { + try { + const { BaseDirectory, readFile } = await import('@tauri-apps/plugin-fs') + return textDecoder.decode( + await readFile(cachePath(key), { baseDir: BaseDirectory.AppLocalData }) + ) + } catch { + return null + } + } + + if (!isStorageAvailable()) return null + return window.localStorage.getItem(storageKey(key)) +} + +export async function writeCacheText(key: string, value: string): Promise { + if (isTauriRuntime()) { + const { BaseDirectory, mkdir, writeFile } = await import('@tauri-apps/plugin-fs') + await mkdir(APP_CACHE_DIR, { baseDir: BaseDirectory.AppLocalData, recursive: true }) + await writeFile(cachePath(key), textEncoder.encode(value), { + baseDir: BaseDirectory.AppLocalData + }) + return + } + + if (!isStorageAvailable()) return + window.localStorage.setItem(storageKey(key), value) +} + +export async function removeCacheEntry(key: string): Promise { + if (isTauriRuntime()) { + try { + const { BaseDirectory, remove } = await import('@tauri-apps/plugin-fs') + await remove(cachePath(key), { baseDir: BaseDirectory.AppLocalData }) + } catch (error) { + console.warn(`Cache delete skipped for "${key}":`, error) + } + return + } + + if (!isStorageAvailable()) return + window.localStorage.removeItem(storageKey(key)) +} + +export async function readCacheBytes(key: string): Promise { + if (!isTauriRuntime()) return null + + try { + const { BaseDirectory, readFile } = await import('@tauri-apps/plugin-fs') + const data = await readFile(cachePath(key), { baseDir: BaseDirectory.AppLocalData }) + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) + } catch { + return null + } +} + +export async function writeCacheBytes(key: string, value: ArrayBuffer): Promise { + if (!isTauriRuntime()) return + + const { BaseDirectory, mkdir, writeFile } = await import('@tauri-apps/plugin-fs') + await mkdir(APP_CACHE_DIR, { baseDir: BaseDirectory.AppLocalData, recursive: true }) + await writeFile(cachePath(key), new Uint8Array(value), { baseDir: BaseDirectory.AppLocalData }) +} + +export async function removeCachePrefix(prefix: string): Promise { + if (isTauriRuntime()) { + try { + const { BaseDirectory, remove } = await import('@tauri-apps/plugin-fs') + await remove(cachePath(prefix), { baseDir: BaseDirectory.AppLocalData, recursive: true }) + } catch (error) { + console.warn(`Cache prefix delete skipped for "${prefix}":`, error) + } + return + } + + if (!isStorageAvailable()) return + for (let i = window.localStorage.length - 1; i >= 0; i--) { + const key = window.localStorage.key(i) + if (key?.startsWith(storageKey(prefix))) window.localStorage.removeItem(key) + } +} + +type JsonCacheEnvelope = { + updatedAt: number + value: T +} + +export async function readCacheJson(key: string, maxAgeMs?: number): Promise { + const raw = await readCacheText(key) + if (!raw) return null + + try { + const envelope = JSON.parse(raw) as Partial> + if (typeof envelope.updatedAt !== 'number' || !('value' in envelope)) return null + if (maxAgeMs !== undefined && Date.now() - envelope.updatedAt > maxAgeMs) return null + return envelope.value as T + } catch { + return null + } +} + +export async function writeCacheJson(key: string, value: unknown): Promise { + await writeCacheText(key, JSON.stringify({ updatedAt: Date.now(), value })) +} diff --git a/src/app/editor/fonts/cache.ts b/src/app/editor/fonts/cache.ts index 1b1b05f43..2fc122915 100644 --- a/src/app/editor/fonts/cache.ts +++ b/src/app/editor/fonts/cache.ts @@ -1,5 +1,13 @@ import type { DownloadedFontCache } from '@open-pencil/core/text' +import { + readCacheBytes, + readCacheJson, + removeCachePrefix, + writeCacheBytes, + writeCacheJson +} from '@/app/cache' + type FontCacheEntry = { family: string style: string @@ -21,11 +29,10 @@ export interface DownloadedFontCacheSummary { } const CACHE_DIR = 'font-cache/v1' -const MANIFEST_PATH = `${CACHE_DIR}/manifest.json` +const MANIFEST_PATH = `${CACHE_DIR}/manifest` +const FILE_DIR = `${CACHE_DIR}/files` const EMPTY_MANIFEST: FontCacheManifest = { version: 1, entries: {} } - const textEncoder = new TextEncoder() -const textDecoder = new TextDecoder() async function cacheKey(family: string, style: string) { return hashText(`${family}\0${style}`) @@ -45,32 +52,14 @@ function hexDigest(data: ArrayBuffer) { return [...new Uint8Array(data)].map((byte) => byte.toString(16).padStart(2, '0')).join('') } -function isTauriRuntime() { - return 'window' in globalThis && '__TAURI_INTERNALS__' in globalThis.window -} - -function isUnavailableTauriRuntimeError(error: unknown) { - return error instanceof ReferenceError && error.message.includes('window is not defined') -} - async function readManifest(): Promise { - const { BaseDirectory, readFile } = await import('@tauri-apps/plugin-fs') - try { - const data = await readFile(MANIFEST_PATH, { baseDir: BaseDirectory.AppLocalData }) - const parsed = JSON.parse(textDecoder.decode(data)) as Partial - if (parsed.version !== 1 || !parsed.entries) return EMPTY_MANIFEST - return { version: 1, entries: parsed.entries } - } catch { - return { version: 1, entries: {} } - } + const manifest = await readCacheJson>(MANIFEST_PATH) + if (manifest?.version !== 1 || !manifest.entries) return EMPTY_MANIFEST + return { version: 1, entries: manifest.entries } } async function writeManifest(manifest: FontCacheManifest) { - const { BaseDirectory, mkdir, writeFile } = await import('@tauri-apps/plugin-fs') - await mkdir(CACHE_DIR, { baseDir: BaseDirectory.AppLocalData, recursive: true }) - await writeFile(MANIFEST_PATH, textEncoder.encode(JSON.stringify(manifest)), { - baseDir: BaseDirectory.AppLocalData - }) + await writeCacheJson(MANIFEST_PATH, manifest) } export async function downloadedFontCacheSummary(): Promise { @@ -86,64 +75,39 @@ export async function downloadedFontCacheSummary(): Promise { - const { BaseDirectory, remove } = await import('@tauri-apps/plugin-fs') - try { - await remove(CACHE_DIR, { baseDir: BaseDirectory.AppLocalData, recursive: true }) - } catch (error) { - console.warn('Downloaded font cache clear skipped:', error) - } + await removeCachePrefix(CACHE_DIR) } export function createTauriDownloadedFontCache(): DownloadedFontCache { return { async read(family, style) { - if (!isTauriRuntime()) return null - try { - const { BaseDirectory, readFile } = await import('@tauri-apps/plugin-fs') - const manifest = await readManifest() - const entry = manifest.entries[await cacheKey(family, style)] - if (!entry) return null + const manifest = await readManifest() + const entry = manifest.entries[await cacheKey(family, style)] + if (!entry) return null - const data = await readFile(`${CACHE_DIR}/${entry.file}`, { - baseDir: BaseDirectory.AppLocalData - }) - const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) - if (buffer.byteLength !== entry.byteLength) return null - if ((await hashBytes(buffer)) !== entry.sha256) return null - return buffer - } catch (error) { - if (isUnavailableTauriRuntimeError(error)) return null - throw error - } + const buffer = await readCacheBytes(`${FILE_DIR}/${entry.file}`) + if (!buffer) return null + if (buffer.byteLength !== entry.byteLength) return null + if ((await hashBytes(buffer)) !== entry.sha256) return null + return buffer }, async write(family, style, data) { - if (!isTauriRuntime()) return - try { - const { BaseDirectory, mkdir, writeFile } = await import('@tauri-apps/plugin-fs') - await mkdir(CACHE_DIR, { baseDir: BaseDirectory.AppLocalData, recursive: true }) + const key = await cacheKey(family, style) + const sha256 = await hashBytes(data) + const file = `${key}.ttf` + await writeCacheBytes(`${FILE_DIR}/${file}`, data) - const key = await cacheKey(family, style) - const sha256 = await hashBytes(data) - const file = `${key}.ttf` - await writeFile(`${CACHE_DIR}/${file}`, new Uint8Array(data), { - baseDir: BaseDirectory.AppLocalData - }) - - const manifest = await readManifest() - manifest.entries[key] = { - family, - style, - file, - byteLength: data.byteLength, - sha256, - updatedAt: Date.now() - } - await writeManifest(manifest) - } catch (error) { - if (isUnavailableTauriRuntimeError(error)) return - throw error + const manifest = await readManifest() + manifest.entries[key] = { + family, + style, + file, + byteLength: data.byteLength, + sha256, + updatedAt: Date.now() } + await writeManifest(manifest) } } } diff --git a/src/components/AppMenu.vue b/src/components/AppMenu.vue index 3e1e7e998..6b8ddfac0 100644 --- a/src/components/AppMenu.vue +++ b/src/components/AppMenu.vue @@ -20,6 +20,7 @@ import { import IconChevronRight from '~icons/lucide/chevron-right' import { vTestId, useI18n } from '@open-pencil/vue' +import AppShortcutText from '@/components/ui/AppShortcutText.vue' import { useMenuUI } from '@/components/ui/menu' import { IS_TAURI } from '@/constants' import { useAppMenu } from '@/app/shell/menu/app-menu' @@ -127,9 +128,9 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' }) @select="runMenuAction(sub)" > {{ menuLabel(sub) }} - {{ + {{ menuShortcut(sub) - }} + }} @@ -153,9 +154,9 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' }) @select="runMenuAction(item)" > {{ menuLabel(item) }} - {{ + {{ menuShortcut(item) - }} + }} diff --git a/src/components/AssetsPanel.vue b/src/components/AssetsPanel.vue index 5648a005b..72c631d76 100644 --- a/src/components/AssetsPanel.vue +++ b/src/components/AssetsPanel.vue @@ -15,9 +15,9 @@ import { useI18n } from '@open-pencil/vue' import { nodeIcon } from '@/app/editor/icons' import { useEditorStore } from '@/app/editor/active-store' import { openExternalLink } from '@/app/shell/ui' +import AppInput from '@/components/ui/AppInput.vue' import { useButtonUI } from '@/components/ui/button' import { useDialogUI } from '@/components/ui/dialog' -import { useInputUI } from '@/components/ui/input' import Tip from '@/components/ui/Tip.vue' type LocalAsset = { @@ -41,7 +41,6 @@ const selectedAssetId = ref(null) const previewUrl = ref(null) const previewLoading = ref(false) let previewRequestId = 0 -const input = useInputUI({ size: 'sm' }) const insertButton = useButtonUI({ tone: 'ghost', size: 'iconSm' }) const primaryButton = useButtonUI({ tone: 'accent', size: 'md' }) const dialog = useDialogUI({ content: 'flex w-[720px] max-w-[92vw] flex-col overflow-hidden' }) @@ -179,11 +178,11 @@ function insertSelectedAsset() { {{ panels.assets }}
-
diff --git a/src/components/CanvasMenu.vue b/src/components/CanvasMenu.vue index 35f1f859b..cce1cb71b 100644 --- a/src/components/CanvasMenu.vue +++ b/src/components/CanvasMenu.vue @@ -32,6 +32,7 @@ import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut' import { createCanvasMenuActions } from '@/app/editor/canvas/menu/actions' import { useCanvasContextMenu } from '@/app/editor/canvas/menu/context' import { canvasMenuItemClass, canvasMenuShortcutClass } from '@/app/editor/canvas/menu/model' +import AppShortcutText from '@/components/ui/AppShortcutText.vue' import { menu, useMenuUI } from '@/components/ui/menu' const store = useEditorStore() @@ -88,7 +89,7 @@ function contextCommandIcon(id: EditorCommandId | undefined): Component | undefi @select="execCommand('copy')" > {{ t.copy }}{{ appMenuShortcutLabel('copy') }} + >{{ appMenuShortcutLabel('copy') }} {{ t.cut }}{{ appMenuShortcutLabel('cut') }} + >{{ appMenuShortcutLabel('cut') }} {{ t.pasteHere }}{{ appMenuShortcutLabel('paste') }} + >{{ appMenuShortcutLabel('paste') }} {{ getCommand('selection.duplicate').label }}{{ + >{{ formatShortcut(editorCommandMetadata('selection.duplicate').shortcut) - }} + }} {{ getCommand('selection.delete').label }}{{ - editorCommandMetadata('selection.delete').shortcut - }} + >{{ editorCommandMetadata('selection.delete').shortcut }} diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue index d17d821ef..4a95ff36f 100644 --- a/src/components/ChatPanel.vue +++ b/src/components/ChatPanel.vue @@ -10,6 +10,7 @@ import { activeTab } from '@/app/tabs' import AcpPermissionDialog from '@/components/chat/AcpPermissionDialog.vue' import ChatInput from '@/components/chat/ChatInput.vue' import ChatMessage from '@/components/chat/ChatMessage.vue' +import AppTextButton from '@/components/ui/AppTextButton.vue' import ProviderSetup from '@/components/chat/ProviderSetup.vue' import { useAIChat } from '@/app/ai/chat/use' import { toast } from '@/app/shell/ui' @@ -185,31 +186,31 @@ function handleClearChat() { v-if="messages.length > 0" class="flex shrink-0 items-center gap-1 border-t border-border px-3 py-1" > - - - + diff --git a/src/components/CodePanel.vue b/src/components/CodePanel.vue index d3404b084..ed88efc9d 100644 --- a/src/components/CodePanel.vue +++ b/src/components/CodePanel.vue @@ -9,6 +9,7 @@ import { JSX_REFERENCE, selectionToJSX } from '@open-pencil/core/design-jsx' import { useI18n, useSceneComputed } from '@open-pencil/vue' import { useEditorStore } from '@/app/editor/active-store' +import AppTextButton from '@/components/ui/AppTextButton.vue' import type { JSXFormat } from '@open-pencil/core/design-jsx' @@ -61,33 +62,33 @@ function copyReference() { >
JSX - +
- - +
@@ -112,4 +113,3 @@ function copyReference() { - diff --git a/src/components/CollabPanel/ConnectedRoom.vue b/src/components/CollabPanel/ConnectedRoom.vue index 4d181bdd2..8b5f5c166 100644 --- a/src/components/CollabPanel/ConnectedRoom.vue +++ b/src/components/CollabPanel/ConnectedRoom.vue @@ -1,5 +1,6 @@ diff --git a/src/components/chat/ProviderSettings/ApiTypeSection.vue b/src/components/chat/ProviderSettings/ApiTypeSection.vue index 2cdfecdbf..ec0bef1d1 100644 --- a/src/components/chat/ProviderSettings/ApiTypeSection.vue +++ b/src/components/chat/ProviderSettings/ApiTypeSection.vue @@ -2,6 +2,7 @@ import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui' import { useI18n } from '@open-pencil/vue' +import ProviderSettingsField from '@/components/chat/ProviderSettings/ProviderSettingsField.vue' import { useProviderSettingsContext } from '@/components/chat/ProviderSettings/context' const ctx = useProviderSettingsContext() @@ -9,8 +10,10 @@ const { dialogs } = useI18n() diff --git a/src/components/chat/ProviderSettings/CustomEndpointSection.vue b/src/components/chat/ProviderSettings/CustomEndpointSection.vue index c2cc3f6e8..20ad74bd8 100644 --- a/src/components/chat/ProviderSettings/CustomEndpointSection.vue +++ b/src/components/chat/ProviderSettings/CustomEndpointSection.vue @@ -1,37 +1,52 @@ diff --git a/src/components/chat/ProviderSettings/MaxTokensSection.vue b/src/components/chat/ProviderSettings/MaxTokensSection.vue index 3b2a17ff4..cfed05510 100644 --- a/src/components/chat/ProviderSettings/MaxTokensSection.vue +++ b/src/components/chat/ProviderSettings/MaxTokensSection.vue @@ -1,7 +1,8 @@ diff --git a/src/components/chat/ProviderSettings/ProviderSettingsField.vue b/src/components/chat/ProviderSettings/ProviderSettingsField.vue new file mode 100644 index 000000000..7d6c6e83c --- /dev/null +++ b/src/components/chat/ProviderSettings/ProviderSettingsField.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/chat/ProviderSettings/ProviderSettingsInput.vue b/src/components/chat/ProviderSettings/ProviderSettingsInput.vue new file mode 100644 index 000000000..7e078cfad --- /dev/null +++ b/src/components/chat/ProviderSettings/ProviderSettingsInput.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/components/chat/ProviderSettings/ProviderSettingsKeyField.vue b/src/components/chat/ProviderSettings/ProviderSettingsKeyField.vue new file mode 100644 index 000000000..763d706ae --- /dev/null +++ b/src/components/chat/ProviderSettings/ProviderSettingsKeyField.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/chat/ProviderSettings/ProviderSettingsLink.vue b/src/components/chat/ProviderSettings/ProviderSettingsLink.vue new file mode 100644 index 000000000..9e3e8067f --- /dev/null +++ b/src/components/chat/ProviderSettings/ProviderSettingsLink.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/chat/ProviderSettings/StockPhotoKeysSection.vue b/src/components/chat/ProviderSettings/StockPhotoKeysSection.vue index f7ec39b66..259e32d3b 100644 --- a/src/components/chat/ProviderSettings/StockPhotoKeysSection.vue +++ b/src/components/chat/ProviderSettings/StockPhotoKeysSection.vue @@ -1,8 +1,7 @@ diff --git a/src/components/chat/ProviderSetup.vue b/src/components/chat/ProviderSetup.vue index 4a238b456..db2e82b0d 100644 --- a/src/components/chat/ProviderSetup.vue +++ b/src/components/chat/ProviderSetup.vue @@ -2,7 +2,8 @@ import { computed, ref } from 'vue' import ProviderSelectField from '@/components/chat/ProviderSelect/ProviderSelectField.vue' -import { useInputUI } from '@/components/ui/input' +import AppInput from '@/components/ui/AppInput.vue' +import AppTextButton from '@/components/ui/AppTextButton.vue' import { useAIChat } from '@/app/ai/chat/use' import { ACP_AGENTS } from '@open-pencil/core/constants' import { openExternalLink } from '@/app/shell/ui' @@ -44,32 +45,27 @@ function save() {
- - + - - + - +