feat: initialize AIUI monorepo with project rules and core types

Foundation for the next-generation AI content surface UI:
- 16 Cursor rules files covering philosophy, Vue conventions, design system,
  content surfaces, plugin system, AI integration, renderers, security,
  Bitcoin-only policy, dev/prod modes, accessibility, performance, animation,
  mobile UX, and git workflow
- pnpm workspaces + Turborepo monorepo (@aiui/core, @aiui/app)
- Vue 3 + Vite + TypeScript + Tailwind CSS 4
- Core type system: plugins, renderers, messages, content blocks
- Plugin registry with renderer registration
- 50 mock film fixtures with search/filter utilities
- App shell with chat page layout
- Environment config templates

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 14:15:39 +00:00
commit c28e6dd811
44 changed files with 5384 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
{
"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": {
"@vitejs/plugin-vue": "latest",
"vite": "latest",
"vue": "latest",
"vue-tsc": "latest",
"vitest": "latest",
"eslint": "latest",
"typescript": "~5.8.0"
}
}
+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
}
+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'
+51
View File
@@ -0,0 +1,51 @@
import { 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 = readonly(plugins)
export const rendererRegistry = readonly(renderers)
+35
View File
@@ -0,0 +1,35 @@
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'
name: string
url: string
quality?: string
icon: string
}
export interface FilmRendererData {
films: Film[]
query?: string
totalResults?: number
}
+30
View File
@@ -0,0 +1,30 @@
import type { ContentBlock } from './content'
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'
}
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
}
+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>)
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": 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' },
},
},
},
})