Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit b67e1527a2
2068 changed files with 472303 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.ts'],
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
{
ignores: ['dist/', 'node_modules/'],
},
)
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@aiui/core",
"version": "0.1.0",
"description": "AIUI core component library - rich AI content surface renderers",
"license": "MIT",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./components/*": "./src/components/*",
"./composables/*": "./src/composables/*",
"./plugins/*": "./src/plugins/*",
"./types/*": "./src/types/*",
"./styles/*": "./src/styles/*"
},
"scripts": {
"dev": "vite build --watch",
"build": "vue-tsc --noEmit && vite build",
"test": "vitest run",
"lint": "eslint src/",
"typecheck": "vue-tsc --noEmit",
"clean": "rm -rf dist"
},
"peerDependencies": {
"vue": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@vitejs/plugin-vue": "^6.0.4",
"eslint": "^10.0.2",
"typescript": "~5.8.0",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vitest": "^4.0.18",
"vue": "^3.5.29",
"vue-tsc": "^3.2.5"
}
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}
+83
View File
@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest'
import {
registerPlugin,
unregisterPlugin,
getPlugin,
getPluginsByType,
registerRenderer,
getRendererForContentType,
getAllRenderers,
} from './plugins/registry'
import type { AIUIPlugin } from './types/plugin'
import type { RendererDefinition } from './types/renderer'
function createMockPlugin(overrides: Partial<AIUIPlugin> = {}): AIUIPlugin {
return {
id: 'test-plugin',
name: 'Test Plugin',
version: '1.0.0',
type: 'ai-provider',
async init() {},
async destroy() {},
async isAvailable() { return true },
...overrides,
}
}
describe('core exports', () => {
it('exports plugin registry functions', () => {
expect(registerPlugin).toBeTypeOf('function')
expect(unregisterPlugin).toBeTypeOf('function')
expect(getPlugin).toBeTypeOf('function')
expect(getPluginsByType).toBeTypeOf('function')
})
it('exports renderer registry functions', () => {
expect(registerRenderer).toBeTypeOf('function')
expect(getRendererForContentType).toBeTypeOf('function')
expect(getAllRenderers).toBeTypeOf('function')
})
})
describe('plugin registry', () => {
it('registers and retrieves a plugin', () => {
const plugin = createMockPlugin({ id: 'reg-test' })
registerPlugin(plugin)
const retrieved = getPlugin('reg-test')
expect(retrieved).toBeDefined()
expect(retrieved?.id).toBe('reg-test')
unregisterPlugin('reg-test')
})
it('unregisters a plugin', () => {
const plugin = createMockPlugin({ id: 'unreg-test' })
registerPlugin(plugin)
unregisterPlugin('unreg-test')
expect(getPlugin('unreg-test')).toBeUndefined()
})
it('filters plugins by type', () => {
const p1 = createMockPlugin({ id: 'type-a', type: 'ai-provider' })
const p2 = createMockPlugin({ id: 'type-b', type: 'storage' })
registerPlugin(p1)
registerPlugin(p2)
const providers = getPluginsByType('ai-provider')
expect(providers.some(p => p.id === 'type-a')).toBe(true)
expect(providers.some(p => p.id === 'type-b')).toBe(false)
unregisterPlugin('type-a')
unregisterPlugin('type-b')
})
it('skips duplicate registration', () => {
const plugin = createMockPlugin({ id: 'dup-test' })
registerPlugin(plugin)
registerPlugin(plugin) // should warn but not throw
unregisterPlugin('dup-test')
})
})
+5
View File
@@ -0,0 +1,5 @@
export * from './types/plugin'
export * from './types/renderer'
export * from './types/message'
export * from './types/content'
export * from './plugins/registry'
@@ -0,0 +1,51 @@
import { type DeepReadonly, type Ref, ref, readonly } from 'vue'
import type { AIUIPlugin, PluginType } from '../types/plugin'
import type { RendererDefinition } from '../types/renderer'
const plugins = ref<Map<string, AIUIPlugin>>(new Map())
const renderers = ref<Map<string, RendererDefinition>>(new Map())
export function registerPlugin(plugin: AIUIPlugin): void {
if (plugins.value.has(plugin.id)) {
console.warn(`Plugin "${plugin.id}" is already registered. Skipping.`)
return
}
plugins.value.set(plugin.id, plugin)
}
export function unregisterPlugin(pluginId: string): void {
plugins.value.delete(pluginId)
}
export function getPlugin<T extends AIUIPlugin>(pluginId: string): T | undefined {
return plugins.value.get(pluginId) as T | undefined
}
export function getPluginsByType<T extends AIUIPlugin>(type: PluginType): T[] {
return Array.from(plugins.value.values()).filter(
(p) => p.type === type
) as T[]
}
export function registerRenderer(renderer: RendererDefinition): void {
if (renderers.value.has(renderer.id)) {
console.warn(`Renderer "${renderer.id}" is already registered. Skipping.`)
return
}
renderers.value.set(renderer.id, renderer)
}
export function getRendererForContentType(
contentType: string
): RendererDefinition | undefined {
return Array.from(renderers.value.values()).find(
(r) => r.contentType === contentType
)
}
export function getAllRenderers(): RendererDefinition[] {
return Array.from(renderers.value.values())
}
export const pluginRegistry: DeepReadonly<Ref<Map<string, AIUIPlugin>>> = readonly(plugins)
export const rendererRegistry: DeepReadonly<Ref<Map<string, RendererDefinition>>> = readonly(renderers)
+165
View File
@@ -0,0 +1,165 @@
export interface ContentBlock {
contentType: string
data: Record<string, unknown>
title?: string
}
export interface Film {
id: string
title: string
year: number
posterUrl: string
backdropUrl?: string
synopsis: string
genres: string[]
rating: number
runtime: number
director: string
cast: string[]
trailerUrl?: string
sources: FilmSource[]
}
export interface FilmSource {
type: 'plex' | 'nextcloud' | 'youtube' | 'free-web' | 'indeehub'
name: string
url: string
quality?: string
icon: string
}
export interface FilmRendererData {
films: Film[]
query?: string
totalResults?: number
}
export interface SongSource {
type: 'plex' | 'spotify' | 'youtube' | 'apple-music' | 'bandcamp' | 'soundcloud' | 'wavlake' | 'internet_archive' | 'jamendo' | 'odysee' | 'funkwhale'
name: string
url: string
icon?: string
}
export interface Song {
id: string
title: string
artist: string
album?: string
year?: number
coverUrl?: string
duration?: number
genres?: string[]
sources?: SongSource[]
}
export interface PodcastSource {
type: 'fountain' | 'rumble' | 'youtube' | 'podcastindex' | 'castopod' | 'odysee' | 'podverse' | 'ipfs' | 'rss'
name: string
url: string
icon?: string
}
export interface Podcast {
id: string
title: string
host?: string
description?: string
coverUrl?: string
year?: number
episodeCount?: number
genres?: string[]
sources: PodcastSource[]
}
export interface PodcastRendererData {
podcasts: Podcast[]
query?: string
totalResults?: number
}
export interface TVSeries {
id: string
title: string
year?: number
endYear?: number
posterUrl?: string
backdropUrl?: string
synopsis?: string
genres?: string[]
rating?: number
seasons?: number
episodes?: number
status?: 'ongoing' | 'ended' | 'cancelled' | 'upcoming'
network?: string
creator?: string
cast?: string[]
sources?: TVSeriesSource[]
}
export interface TVSeriesSource {
type: 'plex' | 'nextcloud' | 'youtube' | 'netflix' | 'free-web' | 'local'
name: string
url: string
quality?: string
icon?: string
}
export interface Book {
id: string
title: string
author: string
year?: number
coverUrl?: string
description?: string
genres?: string[]
pages?: number
isbn?: string
rating?: number
sources?: BookSource[]
}
export interface BookSource {
type: 'openlibrary' | 'gutenberg' | 'archive' | 'goodreads' | 'libgen' | 'local'
name: string
url: string
icon?: string
}
export interface ImageItem {
id: string
url: string
title?: string
description?: string
alt?: string
width?: number
height?: number
source?: string
attribution?: string
}
export interface Place {
id: string
name: string
address?: string
city?: string
cuisine?: string
category?: string
rating?: number
priceLevel?: number // 1-4 ($-$$$$)
phone?: string
website?: string
hours?: string
description?: string
photoUrl?: string
lat?: number
lng?: number
sources?: PlaceSource[]
}
export interface PlaceSource {
type: 'gmaps' | 'osm' | 'yelp' | 'tripadvisor' | 'foursquare' | 'local'
name: string
url: string
icon?: string
}
+67
View File
@@ -0,0 +1,67 @@
import type { ContentBlock } from './content'
export interface WebSearchResult {
title: string
url: string
content?: string
/** Image/thumbnail URL from search engine (e.g. SearXNG img_src) */
imgSrc?: string
}
export interface ImageAttachment {
/** Base64-encoded image data (no data: prefix) */
data: string
/** MIME type e.g. image/jpeg, image/png, image/gif, image/webp */
mediaType: string
}
export interface Message {
id: string
role: 'user' | 'assistant' | 'system'
content: string
contentBlocks?: ContentBlock[]
timestamp: number
model?: string
usage?: { promptTokens: number; completionTokens: number }
replyTo?: string
reactions?: Reaction[]
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error'
/** Web search results (articles, links) attached when query used web search */
webResults?: WebSearchResult[]
/** Timestamp of last edit (set when user edits a sent message) */
editedAt?: number
/** Attached images (base64, max 4) for vision-capable models */
images?: ImageAttachment[]
/** User feedback: 'up' = thumbs up, 'down' = thumbs down */
feedback?: 'up' | 'down'
}
export interface Reaction {
emoji: string
userId: string
timestamp: number
}
export interface Conversation {
id: string
title: string
messages: Message[]
createdAt: number
updatedAt: number
model?: string
systemPrompt?: string
/** ID of parent conversation this was branched from */
parentConversationId?: string
/** Message ID in parent where this branch forked */
branchPoint?: string
/** IDs of child branches forked from this conversation */
childBranchIds?: string[]
/** ID of the persona applied to this conversation */
personaId?: string
/** Generation params (persisted per conversation) */
temperature?: number
maxTokens?: number
topP?: number
/** Comma-separated stop sequences */
stopSequences?: string[]
}
+169
View File
@@ -0,0 +1,169 @@
export type PluginType =
| 'ai-provider'
| 'media-source'
| 'messaging'
| 'storage'
| 'renderer'
| 'file-handler'
| 'crypto'
| 'search'
| 'auth'
| 'wallet'
| 'social-embed'
| 'mcp'
| 'media'
export interface PluginContext {
settings: PluginSettingsStore
events: PluginEventBus
logger: PluginLogger
}
export interface PluginSettingsStore {
get<T>(key: string): T | undefined
set<T>(key: string, value: T): void
}
export interface PluginEventBus {
emit(event: string, payload?: unknown): void
on(event: string, handler: (payload?: unknown) => void): () => void
}
export interface PluginLogger {
info(message: string, ...args: unknown[]): void
warn(message: string, ...args: unknown[]): void
error(message: string, ...args: unknown[]): void
}
export interface AIUIPlugin {
id: string
name: string
version: string
type: PluginType
description?: string
icon?: string
init(context: PluginContext): Promise<void>
destroy(): Promise<void>
isAvailable(): Promise<boolean>
}
export interface AIProviderAdapter extends AIUIPlugin {
type: 'ai-provider'
chat(messages: ChatMessage[], options: ChatOptions): AsyncIterable<ChatChunk>
models(): Promise<AIModel[]>
supportsStreaming: boolean
supportsVision: boolean
supportsTools: boolean
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | ContentPart[]
toolCalls?: ToolCall[]
toolCallId?: string
}
export interface ContentPart {
type: 'text' | 'image_url'
text?: string
imageUrl?: string
}
export interface ChatOptions {
model: string
temperature?: number
maxTokens?: number
tools?: ToolDefinition[]
stream?: boolean
}
export interface ChatChunk {
type: 'text' | 'tool_call' | 'done' | 'error'
text?: string
toolCall?: ToolCall
error?: string
usage?: { promptTokens: number; completionTokens: number }
}
export interface ToolCall {
id: string
name: string
arguments: Record<string, unknown>
}
export interface ToolDefinition {
name: string
description: string
parameters: Record<string, unknown>
}
export interface ToolResult {
toolCallId: string
content: string
isError: boolean
}
export interface AIModel {
id: string
name: string
provider: string
supportsVision: boolean
supportsTools: boolean
contextWindow: number
}
export interface MediaSourcePlugin extends AIUIPlugin {
type: 'media-source'
search(query: string): Promise<MediaItem[]>
getLibrary(filters?: Record<string, unknown>): Promise<MediaItem[]>
getPlayUrl(itemId: string): Promise<string>
}
export interface MediaItem {
id: string
title: string
type: 'film' | 'tv' | 'music' | 'podcast' | 'audiobook'
posterUrl?: string
year?: number
rating?: number
source: string
sourceIcon?: string
}
export interface WalletPlugin extends AIUIPlugin {
type: 'wallet'
supports: PaymentMethod[]
isInstalled(): Promise<boolean>
getPayUri(request: PaymentRequest): string
openWallet(request: PaymentRequest): Promise<void>
}
export type PaymentMethod = 'lightning' | 'onchain' | 'cashu' | 'fedimint'
export interface PaymentRequest {
type: PaymentMethod
invoice?: string
address?: string
amount?: number
memo?: string
lnurl?: string
cashuToken?: string
mintUrl?: string
}
export interface SocialEmbedPlugin extends AIUIPlugin {
type: 'social-embed'
platform: 'x' | 'nostr' | 'mastodon' | 'bluesky'
fetchPost(url: string): Promise<SocialPost>
fetchThread(url: string): Promise<SocialPost[]>
}
export interface SocialPost {
id: string
author: { name: string; handle: string; avatarUrl: string }
content: string
media?: { type: 'image' | 'video'; url: string }[]
metrics?: { likes: number; reposts: number; replies: number }
timestamp: string
url: string
}
+20
View File
@@ -0,0 +1,20 @@
import type { Component } from 'vue'
export type SurfaceType =
| 'chat-preview'
| 'chat-play'
| 'panel-preview'
| 'panel-play'
| 'panel-edit'
export interface RendererDefinition {
id: string
name: string
contentType: string
surfaces: SurfaceType[]
chatPreview?: Component | (() => Promise<Component>)
chatPlay?: Component | (() => Promise<Component>)
panelPreview?: Component | (() => Promise<Component>)
panelPlay?: Component | (() => Promise<Component>)
panelEdit?: Component | (() => Promise<Component>)
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"noEmit": false,
"declaration": true,
"declarationMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}
+26
View File
@@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'AIUICore',
formats: ['es'],
fileName: 'aiui-core',
},
rollupOptions: {
external: ['vue'],
output: {
globals: { vue: 'Vue' },
},
},
},
})
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
test: {
globals: true,
},
})