openpencil/src/composables/use-chat.ts
Danila Poyarkov af9b9fd0fa Replace store thunks with a late-binding proxy
useEditorStore() now returns a Proxy that delegates to the current
active store at access time. No more store() wrapper functions,
try/catch guards, or passing stores through function parameters.
2026-03-02 18:36:27 +03:00

107 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Chat } from '@ai-sdk/vue'
import { DEFAULT_AI_MODEL } from '@open-pencil/core'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { DirectChatTransport, ToolLoopAgent } from 'ai'
import dedent from 'dedent'
import { computed, ref, watch } from 'vue'
import { createAITools } from '@/ai/tools'
import { useEditorStore } from '@/stores/editor'
import type { UIMessage } from 'ai'
export { AI_MODELS as MODELS } from '@open-pencil/core'
export type { ModelOption } from '@open-pencil/core'
const API_KEY_STORAGE = 'open-pencil:openrouter-api-key'
const MODEL_STORAGE = 'open-pencil:model'
const SYSTEM_PROMPT = dedent`
You are a design assistant inside OpenPencil, a Figma-like design editor.
Help users create and modify designs. Be concise and direct.
When describing changes, use specific design terminology.
Available node types: FRAME (containers/cards), RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION.
Colors can be hex strings (#ff0000) or RGBA objects with values 0–1.
Coordinates use canvas space — (0, 0) is the top-left of the page.
Always use tools to make changes. After creating nodes, briefly describe what you did.
When the user asks to create a layout, use create_shape with FRAME, then set_layout for auto-layout.
`
const apiKey = ref(localStorage.getItem(API_KEY_STORAGE) ?? '')
const modelId = ref(localStorage.getItem(MODEL_STORAGE) ?? DEFAULT_AI_MODEL)
const activeTab = ref<'design' | 'ai'>('design')
watch(apiKey, (key) => {
if (key) {
localStorage.setItem(API_KEY_STORAGE, key)
} else {
localStorage.removeItem(API_KEY_STORAGE)
}
})
watch(modelId, (id) => {
localStorage.setItem(MODEL_STORAGE, id)
})
const isConfigured = computed(() => apiKey.value.length > 0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test-only mock transports don't implement full generics
let overrideTransport: (() => any) | null = null
let chat: Chat<UIMessage> | null = null
function createTransport() {
if (overrideTransport) return overrideTransport()
const openrouter = createOpenRouter({
apiKey: apiKey.value,
headers: {
'X-OpenRouter-Title': 'OpenPencil',
'HTTP-Referer': 'https://github.com/open-pencil/open-pencil'
}
})
const tools = createAITools(useEditorStore())
const agent = new ToolLoopAgent({
model: openrouter(modelId.value),
instructions: SYSTEM_PROMPT,
tools
})
return new DirectChatTransport({ agent })
}
function ensureChat(): Chat<UIMessage> | null {
if (!apiKey.value) return null
if (!chat) {
chat = new Chat<UIMessage>({
transport: createTransport()
})
}
return chat
}
function resetChat() {
chat = null
}
if (typeof window !== 'undefined') {
window.__OPEN_PENCIL_SET_TRANSPORT__ = (factory) => {
overrideTransport = factory
}
}
export function useAIChat() {
return {
apiKey,
modelId,
activeTab,
isConfigured,
ensureChat,
resetChat
}
}