Files

101 lines
7.6 KiB
HTML

<!doctype html><html><head><style>html,body,#app,#app>div{height:100%;margin:0}</style></head><body><div id="app"></div><script type="module">
import '/src/style.css'
import { createApp, h, ref, KeepAlive, markRaw, nextTick } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createMemoryHistory } from 'vue-router'
import { createI18n } from 'vue-i18n'
import { enableNativeUI } from '@aiui/app/runtime'
import { rpcClient } from '/src/api/rpc-client.ts'
window.calls = []
rpcClient.call = async ({ method, params }) => {
window.calls.push({ method, params })
if (method === 'ai.permissions.get') return { granted: [] }
if (method === 'system.settings.get') return { value: { schema: 1, settings: { provider: 'local', openai_model: 'fixture' }, local_ready: true } }
if (method === 'assistant.chat') return { text: 'Fixture answer', surfaces: [] }
return {}
}
enableNativeUI()
const { default: Chat } = await import('/src/views/Chat.vue')
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/dashboard/chat', component: Chat }] })
await router.push('/dashboard/chat')
const visible = ref(true)
const generation = ref(0)
window.remountNative = () => { generation.value++ }
window.toggleNative = () => { visible.value = !visible.value }
const payload = ref(null)
const app = createApp({ setup: () => () => h('div', [h(KeepAlive, { key: generation.value }, { default: () => visible.value ? h(Chat) : h('div', 'other tab') }), payload.value && h('div', { class: 'aiui-native', id: 'payload-fixture', style: 'position:fixed;top:12px;left:12px;width:640px;height:360px;z-index:1000;background:#111;overflow:auto' }, [h(payload.value.component, payload.value.props ?? { language: 'javascript', code: payload.value.code }), h('div', { id: 'rich-fixture', innerHTML: payload.value.html })])]) })
app.use(createPinia()).use(router).use(createI18n({ legacy: false, locale: 'en', messages: { en: {} }, missingWarn: false, fallbackWarn: false })).mount('#app')
window.nativeApp = app
window.prepareNativePayload = async () => {
const { sanitizeRichHtml } = await import('@aiui/app/utils/html')
const { default: CodeRunner } = await import('@aiui/app/components/renderers/CodeRunner.vue')
window.richExecuted = false
payload.value = { component: markRaw(CodeRunner), html: sanitizeRichHtml('<img src=x onerror="window.richExecuted=true"><script>window.richExecuted=true<' + '/script><svg onload="window.richExecuted=true"></svg><a href="javascript:window.richExecuted=true">bad</a><p>Safe text</p>'), code: `try { console.log(parent.document.cookie) } catch { console.log('host access blocked') }; parent.postMessage({type:'chat:request',id:'evil',text:'malicious payment'}, '*'); fetch('/rpc/v1').catch(() => console.log('network blocked'))` }
await nextTick()
}
window.prepareUnsafeLink = async () => {
const { default: AppDetail } = await import('@aiui/app/components/content/AppDetail.vue')
window.richExecuted = false
payload.value = { component: markRaw(AppDetail), props: { app: { id: 'fixture', name: 'Unsafe link', url: 'javascript:window.richExecuted=true', platforms: [], category: 'dev' } }, html: '' }
await nextTick()
}
window.prepareVideoSandbox = async () => {
const { createYouTubeSandbox } = await import('@aiui/app/services/youtubeSandbox')
const container = document.createElement('div')
container.id = 'video-sdk-fixture'
container.style.cssText = 'position:fixed;inset:20px;z-index:1000;background:black'
document.body.appendChild(container)
window.sdkEvents = { ready: false, states: [], escaped: null }
window.addEventListener('message', event => { if (event.source === container.querySelector('iframe')?.contentWindow && event.data?.type === 'fixture-sdk-escape') window.sdkEvents.escaped = event.data.escaped })
window.sdkPlayer = createYouTubeSandbox(container, 'dQw4w9WgXcQ', { ready: () => { window.sdkEvents.ready = true }, state: state => window.sdkEvents.states.push(state) })
}
window.preparePdfAndMath = async () => {
const { default: PdfViewer } = await import('@aiui/app/components/renderers/PdfViewer.vue')
const { renderMathInHtml } = await import('@aiui/app/composables/useMathRenderer')
const { sanitizeRichHtml } = await import('@aiui/app/utils/html')
const { katexVersion } = await import('@aiui/app/__tests__/fixtures/patchedRenderers')
payload.value = { component: markRaw(PdfViewer), props: { url: '/e2e/fixtures/native-document.pdf', title: 'Patched PDF fixture' }, html: sanitizeRichHtml(await renderMathInHtml('Safe math $x^2 + y^2 = z^2$ and $\\href{javascript:window.richExecuted=true}{bad}$')) }
window.fixtureKatexVersion = katexVersion
await nextTick()
}
window.extractFixturePdf = async () => {
const { extractPdf } = await import('@aiui/app/__tests__/fixtures/patchedRenderers')
return extractPdf('/e2e/fixtures/native-document.pdf')
}
window.seedNativeStorage = async () => {
const { useChatStore } = await import('@aiui/app/stores/chat')
const store = useChatStore(app.config.globalProperties.$pinia)
while (!store.loaded || !store.conversations.has('aiui-guide')) await new Promise(resolve => setTimeout(resolve, 20))
const { deriveKey, encryptToString } = await import('@aiui/app/utils/crypto')
const salt = new Uint8Array(16).fill(7)
const key = await deriveKey('fixture passphrase', salt)
const conversation = { id: 'preserved', title: 'Preserved conversation', messages: [{ id: 'message-one', role: 'user', content: 'Keep this conversation', timestamp: Date.now() }], createdAt: Date.now(), updatedAt: Date.now() }
const encrypted = await encryptToString(JSON.stringify(conversation), key)
const vault = await encryptToString('fixture-private-key', key)
for (const [name, store, keyPath, value] of [['aiui-store', 'conversations', 'id', { id: 'preserved', updatedAt: conversation.updatedAt, encrypted }], ['aiui-vault', 'api-keys', 'provider', { provider: 'claude', value: vault, encrypted: true }]]) {
await new Promise((resolve, reject) => {
const req = indexedDB.open(name, 1)
req.onupgradeneeded = () => { const table = req.result.createObjectStore(store, { keyPath }); if (name === 'aiui-store') table.createIndex('updatedAt', 'updatedAt') }
req.onsuccess = () => { const tx = req.result.transaction(store, 'readwrite'); tx.objectStore(store).put(value); tx.oncomplete = () => { req.result.close(); resolve() }; tx.onerror = () => reject(tx.error) }
})
}
localStorage.setItem('aiui-crypto-salt', Array.from(salt).map(x => x.toString(16).padStart(2, '0')).join(''))
store.conversations.set('preserved', conversation)
store.activeConversationId = 'preserved'
await nextTick()
localStorage.setItem('aiui-active-conversation', 'preserved')
localStorage.setItem('host-preservation-control', 'unchanged')
return { encrypted, vault }
}
window.nativeChatState = async () => { const { useChatStore } = await import('@aiui/app/stores/chat'); const store = useChatStore(app.config.globalProperties.$pinia); return { active: store.activeConversationId, ids: [...store.conversations.keys()] } }
window.attemptLockedOverwrite = async () => {
const { saveConversation } = await import('@aiui/app/utils/idb-storage')
try { await saveConversation({ id: 'preserved', title: 'overwrite', messages: [], createdAt: 1, updatedAt: 1 }); return false } catch { return true }
}
window.readNativeRecord = async (name, store, id) => new Promise((resolve, reject) => {
const req = indexedDB.open(name)
req.onsuccess = () => { const read = req.result.transaction(store, 'readonly').objectStore(store).get(id); read.onsuccess = () => { req.result.close(); resolve(read.result) }; read.onerror = () => reject(read.error) }
})
</script></body></html>