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:
@@ -0,0 +1,65 @@
|
||||
---
|
||||
description: Core development philosophy for AIUI - the foundational rules that govern all code and design decisions
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Master Philosophy
|
||||
|
||||
## Mission
|
||||
Build the next-generation AI content surface UI — a paradigm where AI responses are rendered as rich, interactive content, not plain text. Delivered as a reusable component library (@aiui/core) and a reference application (AIUI App).
|
||||
|
||||
## Philosophical Pillars
|
||||
|
||||
### 1. Open Source Only
|
||||
Every dependency must be OSS (MIT, Apache-2.0, GPL-compatible). No proprietary SDKs, no vendor-locked services. Before adding any dependency, verify its license.
|
||||
|
||||
### 2. Decentralized-First
|
||||
No hard dependency on any centralized service. AI backends, messaging protocols, storage, search — all connect through pluggable adapter interfaces. Users choose their own providers.
|
||||
|
||||
### 3. Bitcoin Only
|
||||
Bitcoin is the only monetary unit. On-chain, Lightning, ecash (Cashu, Fedimint/Fedi). No fiat payment rails, no altcoins, no stablecoins — anywhere in the UI or codebase. AIUI is never a wallet and never handles funds directly. See `10-bitcoin-only.mdc` for full rules.
|
||||
|
||||
### 4. Cryptography for Everything Sensitive
|
||||
E2E encryption for messages, encrypted local storage, proper key management. Privacy is not a feature — it is a requirement.
|
||||
|
||||
### 5. Mobile-First, Everywhere-Perfect
|
||||
Every component works flawlessly on mobile, tablet, and desktop. Mobile is the foundation, not an afterthought. Touch targets, viewport management, and safe areas are first-class citizens.
|
||||
|
||||
### 6. Consistency is Sacred
|
||||
Mobile and desktop versions show identical content and functionality unless explicitly designed otherwise. Design tokens ensure visual consistency across all breakpoints.
|
||||
|
||||
### 7. Theme-First Architecture
|
||||
Theming is a core architectural decision from day one. Themes are CSS-based with reactive state management. Dark mode and light mode are equals.
|
||||
|
||||
### 8. Utility-First, Component-Second
|
||||
Tailwind CSS utilities in templates for maximum flexibility. Component classes only for truly reusable patterns. Extract components when you repeat, not before.
|
||||
|
||||
### 9. Performance as a Feature
|
||||
Initial load < 250KB gzipped. Lazy load everything that isn't immediately visible. CSS transforms for GPU acceleration. SVG over raster images. Code splitting by default.
|
||||
|
||||
### 10. Plugin-Everything
|
||||
Every external integration connects through a typed plugin interface. AI providers, media sources, messaging protocols, wallets, social embeds — all pluggable.
|
||||
|
||||
### 11. Accessibility is Not Optional
|
||||
WCAG AA compliance minimum. Keyboard navigation everywhere. Screen reader friendly. Color contrast tested and validated.
|
||||
|
||||
### 12. MCP-Native
|
||||
First-class Model Context Protocol support for AI tool interoperability.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Desktop-first thinking
|
||||
- Hardcoded values (use design tokens)
|
||||
- Premature abstraction (build three times before abstracting)
|
||||
- Magic numbers without comments
|
||||
- Invisible state (user should always know what's happening)
|
||||
- Handling funds or private keys
|
||||
- Loading third-party tracking scripts
|
||||
- Proprietary dependencies
|
||||
|
||||
## The Ultimate Goal
|
||||
|
||||
When someone uses AIUI, they should think: "This feels incredibly polished", "Everything just works", "My data is safe", "I control my own setup."
|
||||
|
||||
When a developer reads the code: "This is well organized", "I understand exactly what's happening", "Adding a new renderer is straightforward."
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Vue 3 Composition API conventions and best practices for AIUI
|
||||
globs: "**/*.vue,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Vue 3 Conventions
|
||||
|
||||
## Composition API with `<script setup>`
|
||||
Always use `<script setup lang="ts">`. Never use Options API.
|
||||
|
||||
## Component Organization Order
|
||||
1. Imports — external, then internal
|
||||
2. Props — with TypeScript-style validation
|
||||
3. Emits — explicitly defined
|
||||
4. State (refs and reactive)
|
||||
5. Computed — derived values, always pure
|
||||
6. Watchers — side effects only
|
||||
7. Methods — business logic
|
||||
8. Lifecycle hooks — ordered by execution
|
||||
9. Expose — public API (if needed)
|
||||
|
||||
## File Organization
|
||||
```
|
||||
src/
|
||||
components/
|
||||
ui/ # Primitives (Button, Card, Badge, Input)
|
||||
chat/ # Chat window, message list, input
|
||||
content-panel/ # Side panel for surfaced content
|
||||
renderers/ # Content type renderers
|
||||
layout/ # Shell, split-pane, responsive containers
|
||||
composables/ # Shared composition functions (useTheme, useMedia, useCrypto)
|
||||
stores/ # Pinia stores
|
||||
plugins/ # Plugin system
|
||||
types/ # Shared TypeScript types
|
||||
styles/ # Global CSS, themes, design tokens
|
||||
utils/ # Pure utility functions
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
- Components: PascalCase (`ProjectCard.vue`)
|
||||
- Composables: camelCase, prefixed with "use" (`useTheme.ts`)
|
||||
- Props: camelCase in JS, kebab-case in templates
|
||||
- Boolean props: prefix with `is`, `has`, `can`, `should`
|
||||
- Handler props: prefix with `on` (`onClick`, `onClose`)
|
||||
- Emits: explicit, kebab-case in templates (`project:updated`)
|
||||
|
||||
## Props — Always Validate
|
||||
```typescript
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
count: { type: Number, default: 0 },
|
||||
status: {
|
||||
type: String as PropType<'pending' | 'active' | 'complete'>,
|
||||
default: 'pending'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Never use array-style props: `defineProps(['title', 'count'])`
|
||||
|
||||
## Reactive State
|
||||
- `ref` for primitives and single values
|
||||
- `reactive` for objects with multiple properties
|
||||
- `computed` for derived state (never side effects in computed)
|
||||
- `shallowRef` for large objects that change at top level only
|
||||
|
||||
## Templates — Keep Clean
|
||||
Move complex logic to computed properties or methods. No inline logic in templates. Use `v-if` for infrequent toggles, `v-show` for frequent ones.
|
||||
|
||||
## Composables
|
||||
- One responsibility per composable
|
||||
- Return only what's needed
|
||||
- Handle cleanup in `onUnmounted`
|
||||
- Make composables testable
|
||||
|
||||
## Performance
|
||||
- Lazy load heavy components: `defineAsyncComponent(() => import(...))`
|
||||
- Use `shallowRef` for large lists
|
||||
- Use `:key` with unique identifiers, never index
|
||||
- Avoid reactive objects in templates (create in script)
|
||||
|
||||
## Error Handling
|
||||
Use `onErrorCaptured` for component-level error boundaries. Always handle async errors with try/catch/finally pattern (loading, error, data states).
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
description: Tailwind CSS utility-first styling conventions for AIUI
|
||||
globs: "**/*.vue,**/*.css,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Tailwind CSS Styling
|
||||
|
||||
## Utility-First
|
||||
Use Tailwind utilities directly in templates. Extract to component classes only when a pattern repeats 3+ times.
|
||||
|
||||
## 4px Spacing Grid
|
||||
All spacing follows a 4px base grid:
|
||||
- 1 = 4px, 2 = 8px, 3 = 12px, 4 = 16px, 5 = 20px, 6 = 24px, 8 = 32px, 12 = 48px, 16 = 64px
|
||||
|
||||
## Typography Scale
|
||||
```
|
||||
text-xs = 12px (metadata, timestamps)
|
||||
text-sm = 14px (body text, buttons)
|
||||
text-base = 16px (default body, inputs)
|
||||
text-lg = 18px (subtitles)
|
||||
text-xl = 20px (card titles)
|
||||
text-2xl = 24px (section headings)
|
||||
text-3xl = 30px (page headings)
|
||||
text-4xl = 36px (hero headings)
|
||||
```
|
||||
|
||||
Font weights: `font-normal` (body), `font-medium` (emphasis), `font-semibold` (headings/buttons), `font-bold` (strong emphasis).
|
||||
|
||||
## Border Radius
|
||||
- `rounded-md` (6px) — inputs
|
||||
- `rounded-lg` (8px) — buttons, standard cards
|
||||
- `rounded-2xl` (16px) — modern cards, modals
|
||||
- `rounded-full` — pills, avatars
|
||||
|
||||
## Shadows
|
||||
- `shadow-sm` — subtle elevation
|
||||
- `shadow-md` — standard card
|
||||
- `shadow-lg` — modal, dropdown
|
||||
- `shadow-xl` — hero card
|
||||
|
||||
## Glass Morphism
|
||||
For cards over complex backgrounds:
|
||||
```html
|
||||
<div class="bg-white/10 backdrop-blur-sm border border-white/10 rounded-2xl">
|
||||
```
|
||||
Never use glass morphism on body text containers or form inputs.
|
||||
|
||||
## Responsive — Mobile First
|
||||
Always write base styles for mobile, enhance with breakpoints:
|
||||
```html
|
||||
<div class="text-base md:text-lg lg:text-xl p-4 md:p-6 lg:p-8">
|
||||
```
|
||||
Breakpoints: `sm` (640px), `md` (768px), `lg` (1024px), `xl` (1280px), `2xl` (1536px).
|
||||
|
||||
## Dark Mode
|
||||
Use CSS custom properties for theme switching. Both Tailwind `dark:` and custom theme classes supported:
|
||||
```html
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
```
|
||||
|
||||
## Gradients for Hierarchy
|
||||
Use gradients strategically for emphasis, not decoration:
|
||||
```html
|
||||
<h2 class="bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
|
||||
```
|
||||
|
||||
## Hover States
|
||||
All interactive elements need immediate visual feedback (< 100ms):
|
||||
```html
|
||||
<button class="transition-colors duration-200 hover:bg-primary-dark">
|
||||
```
|
||||
|
||||
## Focus States
|
||||
Always visible focus for accessibility:
|
||||
```html
|
||||
<button class="focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2">
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
description: Design system foundations - colors, typography, spacing, component patterns
|
||||
globs: "**/*.vue,**/*.css,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Design System
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Color Palette
|
||||
Semantic color tokens defined by purpose, not appearance:
|
||||
- `primary` — main brand actions (#606060 / configurable)
|
||||
- `accent` — highlight, Bitcoin orange (#F7931A)
|
||||
- `success` — positive states (#10B981)
|
||||
- `error` — negative states (#EF4444)
|
||||
- `warning` — caution states (#F59E0B)
|
||||
- `info` — informational (#3B82F6)
|
||||
- Neutrals: gray-50 through gray-900
|
||||
|
||||
### Typography
|
||||
- Display font: Geometric sans-serif (Space Grotesk, Inter Display)
|
||||
- Body font: Clean sans-serif (Inter, system-ui)
|
||||
- Mono font: Menlo, Monaco, Courier New
|
||||
- Use single font family with weight variations for simplicity
|
||||
|
||||
### Spacing
|
||||
All spacing on 4px grid. Common patterns:
|
||||
- Component padding: `p-4` (16px) to `p-6` (24px)
|
||||
- Section spacing: `py-12` (48px) to `py-16` (64px)
|
||||
- Element gaps: `gap-4` (16px) default
|
||||
|
||||
### Shadows & Glows
|
||||
```css
|
||||
shadow-soft: 0 2px 8px rgba(0, 0, 0, 0.08)
|
||||
shadow-card: 0 4px 16px rgba(0, 0, 0, 0.1)
|
||||
shadow-elevated: 0 8px 32px rgba(0, 0, 0, 0.15)
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Buttons
|
||||
- Primary: solid background, white text, `rounded-lg`, `font-semibold`
|
||||
- Secondary: border only, primary text, fill on hover
|
||||
- Ghost: text only, subtle background on hover
|
||||
- Icon: `p-2 rounded-md`, minimum 44x44px touch target
|
||||
- Sizes: sm (`px-4 py-2`), md (`px-6 py-3`), lg (`px-8 py-4`)
|
||||
|
||||
### Cards
|
||||
- Background: white/dark with `rounded-2xl` (16px radius)
|
||||
- Padding: `p-6`
|
||||
- Shadow: `shadow-card` default, `shadow-elevated` on hover
|
||||
- Interactive cards get `hover:shadow-lg transition-shadow duration-300 cursor-pointer`
|
||||
|
||||
### Form Elements
|
||||
- Inputs: `min-h-[44px]` touch target, `text-base` (prevents iOS zoom), `rounded-lg`
|
||||
- Labels: `text-sm font-semibold text-gray-700 mb-2`
|
||||
- Error text: `text-sm text-error mt-1`
|
||||
- Focus: `focus:border-primary focus:ring-2 focus:ring-primary/50`
|
||||
|
||||
### Badges
|
||||
- Status badges: `inline-flex items-center px-3 py-1 rounded-full text-sm font-medium`
|
||||
- Color variants: success (green-100/green-800), warning (yellow-100/yellow-800), error (red-100/red-800)
|
||||
|
||||
### Icons
|
||||
- SVG preferred, using Lucide Icons
|
||||
- Sizes: xs (16px), sm (20px), md (24px), lg (32px), xl (48px)
|
||||
- Always use `currentColor` for stroke/fill
|
||||
- Icon-only buttons must have `aria-label`
|
||||
|
||||
## Layout Patterns
|
||||
- Standard container: `max-w-7xl mx-auto px-4 md:px-6`
|
||||
- Reading container: `max-w-3xl mx-auto px-4 md:px-6`
|
||||
- Sidebar + content: `grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-6`
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
description: Component architecture principles - composition, patterns, and structure
|
||||
globs: "**/*.vue,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Component Architecture
|
||||
|
||||
## Core Philosophy: Composition Over Configuration
|
||||
Build complex UIs from simple, focused components that compose well together.
|
||||
|
||||
- Single Responsibility: each component does one thing well
|
||||
- Use slots instead of complex prop APIs
|
||||
- Provide sensible defaults
|
||||
- Clear TypeScript interfaces for props
|
||||
- Keep component state local and minimal
|
||||
|
||||
## Anti-Patterns
|
||||
- God components that do everything
|
||||
- Prop drilling through many layers (use provide/inject or Pinia)
|
||||
- Hard-coded values instead of props
|
||||
- Component logic mixed with layout
|
||||
- Tight coupling between components
|
||||
|
||||
## Compound Component Pattern
|
||||
Components that work together as a cohesive unit:
|
||||
```vue
|
||||
<Card>
|
||||
<Card.Header>Title</Card.Header>
|
||||
<Card.Body>Content</Card.Body>
|
||||
<Card.Footer>Actions</Card.Footer>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Container/Presenter Pattern
|
||||
Separate logic from presentation:
|
||||
- Container: handles data fetching, state, side effects
|
||||
- Presenter: pure rendering, receives data via props, emits events
|
||||
|
||||
## Slot Pattern (Vue)
|
||||
Use named slots for flexible content injection:
|
||||
```vue
|
||||
<template>
|
||||
<div class="section">
|
||||
<slot name="title" />
|
||||
<slot name="content" />
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Prop Interface Design
|
||||
```typescript
|
||||
interface BaseComponentProps {
|
||||
class?: string
|
||||
testId?: string
|
||||
}
|
||||
|
||||
interface ButtonProps extends BaseComponentProps {
|
||||
variant?: 'primary' | 'secondary' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Component File Template
|
||||
```
|
||||
1. Imports (external, then internal)
|
||||
2. Types/Interfaces
|
||||
3. Constants
|
||||
4. Main component (props, emits, state, computed, methods, lifecycle)
|
||||
5. Sub-components (if any)
|
||||
```
|
||||
|
||||
## Error Boundaries
|
||||
Every major section should have error boundary handling via `onErrorCaptured`. Show fallback UI, never a blank screen.
|
||||
|
||||
## Responsive Components
|
||||
Use CSS-based responsive (`hidden md:block`) over JS-based (`useMediaQuery`) when possible. JS-based only when behavior changes (not just visibility).
|
||||
|
||||
## Component Checklist
|
||||
Before shipping any component:
|
||||
- [ ] TypeScript interface defined
|
||||
- [ ] Sensible default props
|
||||
- [ ] Loading and error states handled
|
||||
- [ ] ARIA attributes added
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Responsive behavior tested
|
||||
- [ ] Dark mode styling works
|
||||
- [ ] Touch interactions verified on mobile
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
description: The five content surfaces that define how content is rendered in AIUI
|
||||
globs: "**/renderers/**,**/chat/**,**/content-panel/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Content Surfaces
|
||||
|
||||
AIUI has five distinct surfaces where content can appear. Every renderer must define how it behaves in each applicable surface.
|
||||
|
||||
## Surface 1: Chat Preview
|
||||
- Location: inline in chat message bubble
|
||||
- Max height: ~120px
|
||||
- Purpose: identify content at a glance (thumbnail, title, brief metadata)
|
||||
- Always tappable/clickable to expand to Panel Preview or Panel Play
|
||||
- Lightweight rendering only — no heavy libraries loaded
|
||||
- Examples: film poster thumbnail strip, file icon with name, code snippet (first 5 lines), image thumbnail
|
||||
|
||||
## Surface 2: Chat Play
|
||||
- Location: inline in chat message bubble
|
||||
- Max height: ~200px
|
||||
- Purpose: inline playback without leaving the chat
|
||||
- Must not disrupt chat scrolling
|
||||
- Has an "expand" button to open in Panel Play
|
||||
- Examples: voice note waveform with play button, short video player, audio player, small interactive widget
|
||||
|
||||
## Surface 3: Panel Preview
|
||||
- Location: content panel (beside chat on desktop, overlay on mobile)
|
||||
- No height limit (scrollable within panel)
|
||||
- Purpose: full browsing/exploration experience
|
||||
- Supports: filtering, sorting, searching, pagination
|
||||
- Click items to go to Panel Play or Panel Edit
|
||||
- Examples: film grid (tiled, filterable), image gallery, search results list, document preview, file tree
|
||||
|
||||
## Surface 4: Panel Play
|
||||
- Location: content panel
|
||||
- Purpose: full immersive media playback
|
||||
- Examples: full video player with controls, audio with spectrum visualization, slideshow, trailer playback
|
||||
|
||||
## Surface 5: Panel Edit/Interactive
|
||||
- Location: content panel
|
||||
- Purpose: full interaction and editing
|
||||
- Changes can be sent back to chat as new messages
|
||||
- Examples: code editor (CodeMirror), form filling, approval workflow, spreadsheet editing, diagram creation
|
||||
|
||||
## Surface Transitions
|
||||
```
|
||||
Chat Preview --tap--> Panel Preview --tap item--> Panel Play
|
||||
--tap item--> Panel Edit
|
||||
Chat Play --expand--> Panel Play
|
||||
Panel Edit --submit--> Chat (new message with result)
|
||||
```
|
||||
|
||||
## Renderer Interface
|
||||
Every renderer must export:
|
||||
```typescript
|
||||
interface RendererDefinition {
|
||||
id: string
|
||||
name: string
|
||||
contentType: string // MIME-like type identifier
|
||||
surfaces: SurfaceType[] // which surfaces this renderer supports
|
||||
chatPreview?: Component // Surface 1
|
||||
chatPlay?: Component // Surface 2
|
||||
panelPreview?: Component // Surface 3
|
||||
panelPlay?: Component // Surface 4
|
||||
panelEdit?: Component // Surface 5
|
||||
lazyDependencies?: () => Promise<any> // heavy libs loaded on demand
|
||||
}
|
||||
```
|
||||
|
||||
## Mobile Behavior
|
||||
- On mobile, there is no side-by-side layout
|
||||
- Panel surfaces open as a full-screen overlay or bottom sheet
|
||||
- Chat Preview and Chat Play remain inline
|
||||
- Transition: tap Chat Preview → full-screen Panel Preview (slide up)
|
||||
- Back gesture or button returns to chat
|
||||
|
||||
## Performance Rules
|
||||
- Chat Preview and Chat Play must render with zero lazy-loaded dependencies
|
||||
- Panel surfaces may lazy-load heavy libraries (CodeMirror, pdf.js, etc.)
|
||||
- Never block the chat scroll with renderer loading
|
||||
- Use skeleton/placeholder while panel content loads
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
description: Plugin architecture rules - interfaces, registration, lifecycle, sandboxing
|
||||
globs: "**/plugins/**,**/*.plugin.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Plugin System
|
||||
|
||||
## Philosophy
|
||||
Every external integration connects through a typed plugin interface. No direct coupling to any service, provider, or protocol.
|
||||
|
||||
## Plugin Types
|
||||
```typescript
|
||||
type PluginType =
|
||||
| 'ai-provider' // LLM backends (OpenRouter, Ollama, Claude, etc.)
|
||||
| 'media-source' // Content sources (Plex, YouTube, Nextcloud, Archive.org)
|
||||
| 'messaging' // Chat protocols (Nostr, Matrix, local)
|
||||
| 'storage' // File storage (local FS, IPFS, Nextcloud)
|
||||
| 'renderer' // Custom content renderers
|
||||
| 'file-handler' // File open/preview handlers
|
||||
| 'crypto' // Encryption providers
|
||||
| 'search' // Search backends (SearXNG, local)
|
||||
| 'auth' // Authentication (Nostr keys, DID, passkeys)
|
||||
| 'wallet' // Bitcoin wallet deep-linking (Phoenix, Zeus, Alby, etc.)
|
||||
| 'social-embed' // Social post fetching (X, Nostr, Mastodon)
|
||||
| 'mcp' // Model Context Protocol servers
|
||||
| 'media' // Media processing (ffmpeg.wasm, whisper, TTS)
|
||||
```
|
||||
|
||||
## Base Plugin Interface
|
||||
```typescript
|
||||
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>
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Context
|
||||
Plugins receive a context object with access to:
|
||||
- Settings store (read/write plugin-specific settings)
|
||||
- Event bus (emit/listen for app events)
|
||||
- Logger (structured logging)
|
||||
- Crypto utilities (for encrypting plugin data at rest)
|
||||
|
||||
Plugins do NOT receive:
|
||||
- Direct DOM access (community plugins)
|
||||
- File system access (without explicit capability grant)
|
||||
- Network access to arbitrary hosts (without declaration)
|
||||
|
||||
## Sandboxing Tiers
|
||||
|
||||
### Tier 1: Trusted (built-in, official)
|
||||
Run in main thread with full API access. AI adapters, core renderers, crypto providers.
|
||||
|
||||
### Tier 2: Community
|
||||
Run in sandboxed iframes with `postMessage` API. Custom renderers, themes, visual extensions. Cannot access host DOM, file system, or network directly.
|
||||
|
||||
### Tier 3: External Processes
|
||||
MCP servers, local AI runners. Run as separate processes (Tauri IPC) or connect via HTTP. Isolated by OS process boundary.
|
||||
|
||||
## Plugin Lifecycle
|
||||
1. `register()` — declare plugin to registry
|
||||
2. `init()` — plugin sets up, connects to services
|
||||
3. Active — plugin responds to requests
|
||||
4. `destroy()` — cleanup on disable/uninstall
|
||||
|
||||
## Registration
|
||||
```typescript
|
||||
import { registerPlugin } from '@aiui/core'
|
||||
|
||||
registerPlugin({
|
||||
id: 'ai-openrouter',
|
||||
name: 'OpenRouter',
|
||||
type: 'ai-provider',
|
||||
version: '1.0.0',
|
||||
async init(ctx) { /* setup */ },
|
||||
async destroy() { /* cleanup */ },
|
||||
// ... adapter methods
|
||||
})
|
||||
```
|
||||
|
||||
## Plugin Settings
|
||||
Each plugin can declare settings schema. Settings are stored encrypted and exposed through a standard settings UI.
|
||||
|
||||
## Rules
|
||||
- Every plugin must declare its type
|
||||
- Every plugin must implement `init()` and `destroy()`
|
||||
- Every plugin must implement `isAvailable()` to report its status
|
||||
- Plugins must handle errors gracefully — never crash the host
|
||||
- Community plugins must not load external scripts
|
||||
- All network requests must go through the plugin context (for privacy/proxy control)
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
description: AI adapter patterns, streaming, tool calling, context injection
|
||||
globs: "**/ai/**,**/plugins/ai-*/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# AI Integration
|
||||
|
||||
## Universal AI Adapter
|
||||
All AI providers connect through the `AIProviderAdapter` interface:
|
||||
|
||||
```typescript
|
||||
interface AIProviderAdapter extends AIUIPlugin {
|
||||
type: 'ai-provider'
|
||||
chat(messages: Message[], options: ChatOptions): AsyncIterable<ChatChunk>
|
||||
models(): Promise<Model[]>
|
||||
supportsStreaming: boolean
|
||||
supportsVision: boolean
|
||||
supportsTools: boolean
|
||||
supportsMultimodal: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Provider Hierarchy
|
||||
1. **OpenAI-Compatible Adapter** — covers OpenRouter, Ollama, vLLM, llama.cpp, LocalAI, Mistral, DeepSeek, xAI, Qwen. Just change `baseURL` + API key.
|
||||
2. **Anthropic Adapter** — Claude. Different tool_use format (content blocks vs tool_calls).
|
||||
3. **Gemini Adapter** — Google. Different multimodal format.
|
||||
4. **MCP Client** — connects to any MCP server for tools, resources, prompts.
|
||||
|
||||
## Streaming
|
||||
- All AI responses use Server-Sent Events (SSE) over HTTP
|
||||
- Pattern: `data: {"token": "Hello"}\n\n` with `data: [DONE]\n\n` termination
|
||||
- Client: parse SSE stream, feed tokens to `StreamingTextRenderer`
|
||||
- Always show a typing indicator while waiting for first token
|
||||
- Handle connection drops gracefully (show error, offer retry)
|
||||
|
||||
## Tool Calling
|
||||
AI can invoke tools. The adapter normalizes tool call formats:
|
||||
```typescript
|
||||
interface ToolCall {
|
||||
id: string
|
||||
name: string
|
||||
arguments: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ToolResult {
|
||||
toolCallId: string
|
||||
content: string | StructuredContent
|
||||
isError: boolean
|
||||
}
|
||||
```
|
||||
|
||||
Normalize across providers:
|
||||
- OpenAI: `tool_calls` in assistant message → `role: "tool"` result
|
||||
- Claude: `type: "tool_use"` content block → `tool_result` in user message
|
||||
- Map both to AIUI's unified `ToolCall` / `ToolResult` types
|
||||
|
||||
## Context Injection
|
||||
The system prompt includes context about the user's environment:
|
||||
- Connected media sources and their capabilities
|
||||
- Available tools and plugins
|
||||
- User preferences (language, theme, preferred wallet)
|
||||
- In dev mode: mock data summaries
|
||||
|
||||
Never include sensitive data (API keys, passwords) in system prompts.
|
||||
|
||||
## Model Selection
|
||||
Users can switch models within a conversation. The UI shows:
|
||||
- Available models from all connected providers
|
||||
- Model capabilities (vision, tools, streaming)
|
||||
- Cost per token in sats (if applicable)
|
||||
|
||||
## Dev Mode
|
||||
- `VITE_OPENROUTER_API_KEY` in `.env.local`
|
||||
- Free models available (Llama, Mistral via OpenRouter)
|
||||
- Mock tool responses available via dev fixtures
|
||||
- Debug panel shows: raw messages, token count, latency
|
||||
|
||||
## Error Handling
|
||||
- Rate limits: show user-friendly message, auto-retry with backoff
|
||||
- Auth errors: prompt to check API key in settings
|
||||
- Network errors: show offline indicator, queue message for retry
|
||||
- Model errors: show error in chat, suggest alternative model
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
description: How to build content renderers - interfaces, lazy loading, accessibility
|
||||
globs: "**/renderers/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Renderer Development
|
||||
|
||||
## What is a Renderer?
|
||||
A renderer is a set of Vue components that know how to display a specific content type across the five content surfaces (chat-preview, chat-play, panel-preview, panel-play, panel-edit).
|
||||
|
||||
## Renderer Registration
|
||||
```typescript
|
||||
import { registerRenderer } from '@aiui/core'
|
||||
|
||||
registerRenderer({
|
||||
id: 'film',
|
||||
name: 'Film',
|
||||
contentType: 'application/x-aiui-film',
|
||||
surfaces: ['chat-preview', 'panel-preview', 'panel-play'],
|
||||
chatPreview: () => import('./FilmChatPreview.vue'),
|
||||
panelPreview: () => import('./FilmGrid.vue'),
|
||||
panelPlay: () => import('./FilmDetail.vue'),
|
||||
})
|
||||
```
|
||||
|
||||
## Content Type Detection
|
||||
Renderers are matched to content by `contentType` field in the message data:
|
||||
```typescript
|
||||
interface ContentBlock {
|
||||
contentType: string // e.g., 'application/x-aiui-film'
|
||||
data: Record<string, unknown> // renderer-specific data
|
||||
title?: string // human-readable title for panel tab
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Rules
|
||||
1. Chat surfaces (preview, play) must render with ZERO lazy-loaded heavy dependencies
|
||||
2. Panel surfaces may lazy-load libraries (CodeMirror, pdf.js, etc.)
|
||||
3. Use `defineAsyncComponent` for panel components
|
||||
4. Show skeleton/placeholder while loading
|
||||
5. Never block the main thread — use Web Workers for heavy parsing
|
||||
|
||||
## Data Contracts
|
||||
Each renderer defines its expected data shape as a TypeScript interface:
|
||||
```typescript
|
||||
interface FilmRendererData {
|
||||
films: Film[]
|
||||
query?: string
|
||||
filters?: FilmFilters
|
||||
}
|
||||
```
|
||||
Document the interface. Validate incoming data. Show graceful error if data is malformed.
|
||||
|
||||
## Accessibility Requirements
|
||||
- All renderers must be keyboard navigable
|
||||
- Images need alt text
|
||||
- Interactive elements need ARIA labels
|
||||
- Media players need captions/transcripts when available
|
||||
- Focus management when transitioning between surfaces
|
||||
|
||||
## Mobile Behavior
|
||||
- Chat Preview: constrained to message bubble width
|
||||
- Chat Play: full message width, max 200px height
|
||||
- Panel surfaces on mobile: full-screen overlay with back gesture
|
||||
- Touch targets: minimum 44x44px
|
||||
- Swipe gestures where appropriate (image gallery, film cards)
|
||||
|
||||
## Renderer Checklist
|
||||
- [ ] TypeScript data interface defined and exported
|
||||
- [ ] All applicable surfaces implemented
|
||||
- [ ] Lazy loading for heavy dependencies
|
||||
- [ ] Skeleton/placeholder states
|
||||
- [ ] Error state (malformed data)
|
||||
- [ ] Empty state (no data)
|
||||
- [ ] Keyboard navigation
|
||||
- [ ] ARIA labels on interactive elements
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Dark mode compatible
|
||||
- [ ] Transition animations (per motion design rules)
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Cryptography and security rules - E2E encryption, key management, storage
|
||||
globs: "**/crypto/**,**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Security & Cryptography
|
||||
|
||||
## Principles
|
||||
- Privacy is a requirement, not a feature
|
||||
- Zero telemetry, zero analytics unless user explicitly opts in
|
||||
- Never transmit unencrypted sensitive data
|
||||
- Never store plaintext credentials
|
||||
- Minimal data collection — store only what's needed
|
||||
|
||||
## Encryption Stack
|
||||
|
||||
### E2E Message Encryption
|
||||
- Library: **tweetnacl.js** (6KB, audited by Cure53)
|
||||
- Algorithm: XSalsa20-Poly1305 via NaCl `box` (public-key authenticated encryption)
|
||||
- Each conversation has a shared secret derived from key exchange
|
||||
|
||||
### Local Storage Encryption
|
||||
- Library: **Web Crypto API** (native, zero bundle cost)
|
||||
- Algorithm: AES-256-GCM for encrypting IndexedDB values
|
||||
- Key derived from user's master password via PBKDF2 (100K+ iterations)
|
||||
|
||||
### Key Management
|
||||
- **Desktop (Tauri)**: OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
||||
- **Web**: Encrypted IndexedDB with user-derived key
|
||||
- **Nostr compatibility**: secp256k1 keys via @noble/curves, NIP-07 browser extension support
|
||||
- **Passkeys/WebAuthn**: For passwordless authentication
|
||||
|
||||
### Credential Storage
|
||||
- API keys encrypted at rest using AES-256-GCM
|
||||
- Never stored in localStorage (use encrypted IndexedDB or OS keychain)
|
||||
- Never included in logs, error reports, or system prompts
|
||||
- Display as masked values in settings UI (show last 4 chars only)
|
||||
|
||||
## Dev Mode Bypass
|
||||
When `VITE_DISABLE_CRYPTO=true` (dev only):
|
||||
- Skip E2E encryption (messages stored in plain text)
|
||||
- Skip storage encryption (IndexedDB unencrypted)
|
||||
- API keys stored in `.env.local` (gitignored)
|
||||
- This flag must NEVER exist in production builds
|
||||
|
||||
## Security Rules for Code
|
||||
- Never log sensitive data (keys, tokens, passwords, message content)
|
||||
- Never include secrets in error messages
|
||||
- Sanitize all user input before rendering (XSS prevention)
|
||||
- Use Content Security Policy headers
|
||||
- Validate all data from plugins before rendering
|
||||
- Community plugins run in sandboxed iframes (no direct DOM access)
|
||||
- Never eval() or innerHTML with untrusted content
|
||||
|
||||
## Network Security
|
||||
- All external requests over HTTPS only
|
||||
- Certificate pinning for known services (Tauri)
|
||||
- Proxy social media fetches to avoid leaking user IP
|
||||
- No third-party tracking scripts, analytics, or telemetry SDKs
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
description: Bitcoin-only payment and monetary policy - on-chain, Lightning, ecash
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Bitcoin Only
|
||||
|
||||
## Core Rule
|
||||
Bitcoin is the only monetary unit in AIUI. This applies everywhere — UI labels, data models, API responses, documentation, and conversation context.
|
||||
|
||||
## Supported Payment Protocols
|
||||
- **On-chain Bitcoin**: BIP21 URI scheme (`bitcoin:bc1q...?amount=0.001`)
|
||||
- **Lightning Network**: BOLT11 invoices, LNURL-pay, LNURL-withdraw, keysend
|
||||
- **Cashu ecash**: Cashu tokens, mint interactions (`cashu:` URI, `web+cashu:`)
|
||||
- **Fedimint/Fedi**: Federation ecash (`fedi:` URI)
|
||||
- **Nostr Zaps**: NIP-57 Lightning zaps (social tipping)
|
||||
|
||||
## AIUI is NEVER a Wallet
|
||||
|
||||
### Never Do
|
||||
- Store private keys or seed phrases
|
||||
- Sign Bitcoin transactions
|
||||
- Build or broadcast transactions
|
||||
- Track wallet balances
|
||||
- Display transaction history
|
||||
- Create send/receive screens
|
||||
- Implement payment processing logic
|
||||
- Hold funds in custody
|
||||
|
||||
### Always Do
|
||||
- Construct deep-link URIs and hand off to external wallet apps
|
||||
- Detect installed wallet apps (via URI scheme probing or Tauri app detection)
|
||||
- Let users configure preferred wallets in settings
|
||||
- Display payment requests as QR codes with "Open in Wallet" buttons
|
||||
- Show invoice/address details (amount, memo, expiry) as read-only information
|
||||
|
||||
## Wallet Deep-Linking
|
||||
```typescript
|
||||
// Construct URI, open external wallet — that's it
|
||||
const uri = `lightning:${bolt11Invoice}`
|
||||
window.open(uri) // or Tauri shell.open(uri)
|
||||
```
|
||||
|
||||
Supported wallet URI schemes:
|
||||
- `bitcoin:` — BIP21 (any on-chain wallet)
|
||||
- `lightning:` — BOLT11 (any Lightning wallet)
|
||||
- `cashu:` — Cashu tokens
|
||||
- `fedi:` — Fedimint
|
||||
- Wallet-specific: `phoenix://`, `zeus://`, `mutiny://`, `alby://`
|
||||
|
||||
## Denomination
|
||||
- Primary unit: **sats** (1 BTC = 100,000,000 sats)
|
||||
- Display: `1,234 sats` or `₿0.00001234`
|
||||
- User preference: sats or BTC (configurable in settings)
|
||||
- AI cost tracking: show token costs in sats
|
||||
|
||||
## Prohibited
|
||||
- No fiat currencies (USD, EUR, etc.) — not in UI, not in code, not in variable names
|
||||
- No altcoins or tokens
|
||||
- No stablecoins (USDT, USDC, etc.)
|
||||
- No fiat-denominated pricing
|
||||
- No payment processor integrations (Stripe, PayPal, etc.)
|
||||
- No KYC/AML flows
|
||||
|
||||
## Renderer Components
|
||||
- `LightningInvoiceRenderer` — BOLT11 QR + amount + memo + "Open in Wallet"
|
||||
- `BitcoinAddressRenderer` — BIP21 QR + "Open in Wallet"
|
||||
- `CashuTokenRenderer` — ecash token + mint info + "Redeem in Wallet"
|
||||
- `FedimintRenderer` — federation ecash + "Open in Fedi"
|
||||
- `PaymentRequestRenderer` — unified card with payment method options
|
||||
- `ZapRenderer` — Nostr zap display (NIP-57)
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
description: Development vs production configuration, feature flags, mock data patterns
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Dev & Prod Modes
|
||||
|
||||
## Development Mode
|
||||
|
||||
### Environment
|
||||
```env
|
||||
# .env.local (gitignored)
|
||||
VITE_OPENROUTER_API_KEY=sk-or-...
|
||||
VITE_TMDB_API_KEY=...
|
||||
VITE_DEV_MODE=true
|
||||
VITE_MOCK_MEDIA_SOURCES=true
|
||||
VITE_DISABLE_CRYPTO=true
|
||||
```
|
||||
|
||||
### What's Enabled
|
||||
- Hot reload via Vite HMR
|
||||
- Debug panel overlay (AI context, plugin status, renderer registry, message data)
|
||||
- Mock media source plugins (Plex, YouTube, Nextcloud from JSON fixtures)
|
||||
- OpenRouter AI connection (real API, free models available)
|
||||
- Component playground (Storybook/Histoire)
|
||||
- Verbose logging
|
||||
- TypeScript strict mode
|
||||
- All renderers available without lazy loading (for dev speed)
|
||||
|
||||
### What's Disabled
|
||||
- E2E encryption (plain text messages for debugging)
|
||||
- Storage encryption (plain IndexedDB)
|
||||
- Tauri features (dev runs as pure web app)
|
||||
- Production optimizations (tree-shaking, minification)
|
||||
- Service worker / offline mode
|
||||
|
||||
### Mock Data
|
||||
- Film fixtures: 50-100 films with real TMDB poster URLs
|
||||
- Media source mocks: JSON files returning fake Plex/YouTube/Nextcloud responses
|
||||
- Located in: `packages/app/src/mocks/`
|
||||
- Auto-loaded when `VITE_MOCK_MEDIA_SOURCES=true`
|
||||
- Mock data must match production data interfaces exactly
|
||||
|
||||
### Dev Scripts
|
||||
```
|
||||
pnpm dev # Web dev server
|
||||
pnpm dev:desktop # Tauri dev (when needed)
|
||||
pnpm storybook # Component playground
|
||||
pnpm test # Vitest
|
||||
pnpm lint # ESLint + Prettier
|
||||
pnpm typecheck # TypeScript
|
||||
pnpm build # Production build
|
||||
pnpm turbo build # Turborepo cached build
|
||||
```
|
||||
|
||||
## Production Mode
|
||||
|
||||
### What's Enabled
|
||||
- E2E encryption for all messages
|
||||
- Encrypted local storage
|
||||
- Key management via OS keychain (Tauri) or encrypted IndexedDB (web)
|
||||
- User-configured AI providers (settings page)
|
||||
- Real media source connections (Plex API, YouTube, etc.)
|
||||
- Optimized builds (tree-shaken, code-split, minified)
|
||||
- Lazy loading for all heavy renderers
|
||||
- Service worker for offline support
|
||||
- Auto-update (Tauri)
|
||||
|
||||
### What's Disabled
|
||||
- Debug panels
|
||||
- Mock data
|
||||
- Dev logging
|
||||
- Source maps (in distributed builds)
|
||||
- `VITE_DISABLE_CRYPTO` flag (must not exist)
|
||||
|
||||
### Build Targets
|
||||
- Web: Static SPA bundle (< 250KB initial gzipped)
|
||||
- Desktop: Tauri app (macOS .dmg, Windows .msi, Linux .AppImage)
|
||||
- Mobile: Tauri mobile (iOS .ipa, Android .apk)
|
||||
|
||||
## Feature Flags
|
||||
Use composable `useFeatureFlags()`:
|
||||
```typescript
|
||||
const { isDev, isTauri, isMobile, isCryptoEnabled, isMockData } = useFeatureFlags()
|
||||
```
|
||||
|
||||
Gate platform-specific features:
|
||||
```typescript
|
||||
if (isTauri()) {
|
||||
// Native file system access
|
||||
} else {
|
||||
// File System Access API or file picker
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variable Rules
|
||||
- All env vars prefixed with `VITE_` (Vite requirement)
|
||||
- Secrets only in `.env.local` (gitignored)
|
||||
- `.env.example` committed with placeholder values
|
||||
- Never read `process.env` directly — use typed config module
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
description: Accessibility standards - WCAG AA, keyboard navigation, screen readers
|
||||
globs: "**/*.vue"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
## Standard
|
||||
WCAG AA compliance minimum. Target AAA where feasible.
|
||||
|
||||
## Color Contrast
|
||||
- Normal text: 4.5:1 minimum ratio
|
||||
- Large text (18px+ or 14px+ bold): 3:1 minimum
|
||||
- Interactive elements: 3:1 against adjacent colors
|
||||
- Test with browser DevTools accessibility panel
|
||||
|
||||
## Keyboard Navigation
|
||||
- All interactive elements focusable via Tab
|
||||
- Visible focus indicators on every focusable element (`focus:ring-2`)
|
||||
- Escape closes modals, drawers, dropdowns
|
||||
- Arrow keys navigate within lists, grids, tabs
|
||||
- Enter/Space activates buttons and controls
|
||||
- Focus trap inside modals (Tab cycles within modal)
|
||||
|
||||
## Semantic HTML
|
||||
```html
|
||||
<header>, <nav>, <main>, <article>, <aside>, <footer>
|
||||
```
|
||||
Never `<div class="header">`. Use semantic elements.
|
||||
|
||||
## ARIA
|
||||
- Icon-only buttons: `aria-label="Close modal"`
|
||||
- Dynamic content: `aria-live="polite"` for updates
|
||||
- Screen reader only text: `class="sr-only"`
|
||||
- Expandable sections: `aria-expanded="true/false"`
|
||||
- Form fields: `aria-describedby` for help text, `aria-invalid` for errors
|
||||
|
||||
## Images
|
||||
- All `<img>` tags need `alt` text
|
||||
- Decorative images: `alt=""`
|
||||
- Complex images: `aria-describedby` pointing to description
|
||||
|
||||
## Media
|
||||
- Audio/video players: keyboard-accessible controls
|
||||
- Provide transcripts/captions when available
|
||||
- Respect `prefers-reduced-motion` for animations
|
||||
|
||||
## Touch Targets
|
||||
- Minimum: 44x44px (Apple HIG)
|
||||
- Recommended: 48x48px (Material Design)
|
||||
- Minimum 8px gap between adjacent targets
|
||||
|
||||
## Reduced Motion
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
Check in JS: `window.matchMedia('(prefers-reduced-motion: reduce)').matches`
|
||||
|
||||
## Testing
|
||||
- VoiceOver (macOS), TalkBack (Android), NVDA (Windows)
|
||||
- Keyboard-only navigation test
|
||||
- axe DevTools or Lighthouse accessibility audit
|
||||
- High contrast mode test
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: Performance optimization - bundle budget, lazy loading, virtual scrolling
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Performance
|
||||
|
||||
## Bundle Budget
|
||||
- Initial load: **< 250KB gzipped**
|
||||
- Core (Vue + Tailwind + Pinia + Router + chat UI): ~150KB
|
||||
- First renderer batch (markdown, streaming text): ~50KB
|
||||
- Everything else: lazy-loaded on demand
|
||||
|
||||
## Lazy Loading Strategy
|
||||
- Route-based code splitting via Vue Router `() => import(...)`
|
||||
- Renderer components via `defineAsyncComponent`
|
||||
- Heavy libraries loaded only when their renderer is activated:
|
||||
- CodeMirror 6: ~300KB (on code edit)
|
||||
- Monaco: ~5MB (on IDE panel open)
|
||||
- pdf.js: ~400KB (on PDF view)
|
||||
- KaTeX: ~300KB (on math render)
|
||||
- Mermaid: ~200KB (on diagram render)
|
||||
- Leaflet: ~40KB (on map render)
|
||||
- Whisper WASM: ~50MB (on STT activation, cached)
|
||||
- Piper TTS: ~100MB (on TTS activation, cached)
|
||||
|
||||
## Virtual Scrolling
|
||||
- Chat message list uses TanStack Virtual
|
||||
- Dynamic row heights (messages vary in size)
|
||||
- Inverted scroll (newest at bottom, load older on scroll up)
|
||||
- Buffer: render 5 items above and below viewport
|
||||
- Recycle DOM nodes for off-screen messages
|
||||
|
||||
## GPU Acceleration
|
||||
Only animate `transform` and `opacity` — never `width`, `height`, `top`, `left`.
|
||||
Use `will-change` sparingly and remove after animation.
|
||||
|
||||
## Image Optimization
|
||||
- Use `loading="lazy"` on all non-critical images
|
||||
- Provide `srcset` with multiple sizes
|
||||
- Use WebP/AVIF where supported
|
||||
- Skeleton placeholders while loading
|
||||
|
||||
## Network
|
||||
- Preconnect to known API hosts
|
||||
- Preload critical resources
|
||||
- Debounce scroll and resize handlers (100ms)
|
||||
- Batch API requests where possible
|
||||
|
||||
## Memory
|
||||
- Clean up event listeners in `onUnmounted`
|
||||
- Use `shallowRef` for large data sets
|
||||
- Dispose heavy library instances when panel closes
|
||||
- Monitor memory with browser DevTools
|
||||
|
||||
## Core Web Vitals Targets
|
||||
- LCP (Largest Contentful Paint): < 2.5s
|
||||
- FID (First Input Delay): < 100ms
|
||||
- CLS (Cumulative Layout Shift): < 0.1
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
description: Animation principles - timing, easing, stagger, reduced motion
|
||||
globs: "**/*.vue,**/*.css"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Animation & Motion Design
|
||||
|
||||
## Philosophy
|
||||
Every animation serves a purpose: guide attention, provide feedback, show relationships, enhance perceived performance, or add delight. Never animate for decoration alone.
|
||||
|
||||
## Duration Scale
|
||||
```
|
||||
100ms - Instant: micro-feedback (hover states, button press)
|
||||
200ms - Fast: small elements (tooltips, dropdowns)
|
||||
300ms - Moderate: standard UI transitions (modals, cards)
|
||||
500ms - Normal: page sections, complex components
|
||||
600ms - Slow: hero animations, page transitions (max for UI)
|
||||
```
|
||||
Never exceed 600ms for UI element animations.
|
||||
|
||||
## Easing Functions
|
||||
- **ease-out** (90% of animations): elements entering viewport
|
||||
- **ease-in**: elements exiting viewport
|
||||
- **ease-in-out**: elements moving within viewport
|
||||
- **spring**: playful interactions (button press, drag-and-drop)
|
||||
- **linear**: progress bars, loading spinners only
|
||||
|
||||
Custom smooth deceleration: `cubic-bezier(0.16, 1, 0.3, 1)`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Fade & Slide Up (entrance)
|
||||
```css
|
||||
@keyframes fadeSlideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
### Scale & Fade (emphasis)
|
||||
```css
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.8); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
```
|
||||
|
||||
### Hover feedback
|
||||
```css
|
||||
.interactive {
|
||||
transition: transform 0.1s ease, opacity 0.1s ease;
|
||||
}
|
||||
.interactive:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
## Staggered Animations
|
||||
When animating multiple elements, stagger by 50-150ms per item:
|
||||
```css
|
||||
.card { animation-delay: calc(var(--index) * 0.1s); }
|
||||
```
|
||||
Max items in a stagger cascade: 6-8. Total cascade: under 1 second.
|
||||
|
||||
## Reduced Motion
|
||||
Always respect `prefers-reduced-motion`. Provide instant transitions as fallback.
|
||||
|
||||
## Performance
|
||||
- Only animate `transform` and `opacity` (GPU-composited)
|
||||
- Use `will-change` sparingly, remove after animation
|
||||
- Limit simultaneous animations
|
||||
- Use `requestAnimationFrame` for JS animations
|
||||
|
||||
## Loading States
|
||||
- Skeleton shimmer: 2s infinite, `linear-gradient` sweep
|
||||
- Pulse: 2s infinite, opacity 1 → 0.5 → 1
|
||||
- Spinner: 1s infinite linear rotation
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
description: Mobile UX patterns - touch targets, gestures, safe areas, viewport
|
||||
globs: "**/*.vue,**/*.css"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Mobile UX
|
||||
|
||||
## Philosophy
|
||||
Design for mobile first, enhance for desktop. Mobile constraints force focus on essential features.
|
||||
|
||||
## Touch Targets
|
||||
- Minimum: 44x44px (Apple HIG)
|
||||
- Optimal: 48x48px (Material Design)
|
||||
- Minimum gap between targets: 8px
|
||||
- Icon buttons: use padding to reach target size (`p-3` for 20px icon = 44px total)
|
||||
|
||||
## Thumb-Friendly Zones
|
||||
```
|
||||
Top 20%: Hard to reach — header, info
|
||||
Middle 60%: Easy reach — main content
|
||||
Bottom 20%: Natural thumb zone — primary actions
|
||||
```
|
||||
Place primary actions (send, approve, play) in the bottom zone. Use FABs at bottom-center on mobile, bottom-right on desktop.
|
||||
|
||||
## Gestures
|
||||
- Swipe left/right: gallery navigation, dismiss
|
||||
- Swipe down: close overlay/bottom sheet, pull-to-refresh
|
||||
- Long press: context menu, selection
|
||||
- Pinch: zoom on images
|
||||
- Minimum swipe distance: 50px before triggering
|
||||
|
||||
## Viewport & Safe Areas
|
||||
```css
|
||||
/* Dynamic viewport height (avoids iOS address bar issue) */
|
||||
height: 100dvh;
|
||||
|
||||
/* Safe area insets for notched devices */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
```
|
||||
|
||||
## Scroll Behavior
|
||||
- Lock body scroll when modal/drawer is open
|
||||
- `overscroll-behavior: contain` on modal content
|
||||
- `touch-action: manipulation` to prevent zoom on double-tap
|
||||
- `-webkit-overflow-scrolling: touch` for smooth iOS scroll
|
||||
|
||||
## Form Inputs
|
||||
- Font size minimum 16px on inputs (prevents iOS zoom on focus)
|
||||
- Use appropriate `inputmode` (numeric, email, tel, url)
|
||||
- Use `autocomplete` attributes
|
||||
- Auto-focus next input on completion (OTP pattern)
|
||||
|
||||
## Content Panel on Mobile
|
||||
- No side-by-side layout (chat + panel)
|
||||
- Panel opens as full-screen overlay or bottom sheet
|
||||
- Bottom sheet: drag handle at top, swipe down to dismiss
|
||||
- Back gesture or button returns to chat
|
||||
- Maintain scroll position when returning
|
||||
|
||||
## Orientation
|
||||
Support both portrait and landscape. Adjust layout with:
|
||||
```css
|
||||
@media (orientation: landscape) { /* landscape overrides */ }
|
||||
```
|
||||
|
||||
## Performance on Mobile
|
||||
- Test on real devices, not just emulators
|
||||
- Test on 3G/4G connections
|
||||
- Debounce scroll handlers
|
||||
- Lazy load images with `loading="lazy"`
|
||||
- Critical CSS inlined, rest loaded async
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: Git workflow - commit conventions, branching, PR process
|
||||
globs: "**/*"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Git Workflow
|
||||
|
||||
## Commit Messages
|
||||
Format: `type(scope): description`
|
||||
|
||||
Types:
|
||||
- `feat`: new feature
|
||||
- `fix`: bug fix
|
||||
- `refactor`: code restructuring (no behavior change)
|
||||
- `style`: formatting, whitespace (no code change)
|
||||
- `docs`: documentation
|
||||
- `test`: adding/updating tests
|
||||
- `chore`: build, dependencies, tooling
|
||||
- `perf`: performance improvement
|
||||
|
||||
Scope: the package or area (`core`, `app`, `plugin-x`, `renderer-film`, etc.)
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(core): add renderer registry with lazy loading
|
||||
fix(chat): prevent scroll jump on new message
|
||||
refactor(plugin-system): simplify adapter interface
|
||||
chore(deps): update Vue to 3.6
|
||||
```
|
||||
|
||||
## Branching
|
||||
- `main`: production-ready, always deployable
|
||||
- `dev`: integration branch for features
|
||||
- `feat/description`: feature branches (from dev)
|
||||
- `fix/description`: bug fix branches
|
||||
- `release/x.y.z`: release preparation
|
||||
|
||||
## Pull Requests
|
||||
- One feature per PR
|
||||
- Description: what changed, why, how to test
|
||||
- All tests pass
|
||||
- TypeScript strict mode passes
|
||||
- No linter errors
|
||||
- Reviewed before merge
|
||||
|
||||
## Rules
|
||||
- Never force push to `main` or `dev`
|
||||
- Never commit `.env.local` or any secrets
|
||||
- Never commit `node_modules`
|
||||
- Squash merge feature branches to keep history clean
|
||||
- Tag releases with semver: `v1.0.0`
|
||||
@@ -0,0 +1,15 @@
|
||||
# AIUI Development Environment
|
||||
# Copy this file to .env.local and fill in your values
|
||||
|
||||
# AI Provider (OpenRouter - gives access to many models including free ones)
|
||||
# Get your key at: https://openrouter.ai/keys
|
||||
VITE_OPENROUTER_API_KEY=sk-or-your-key-here
|
||||
|
||||
# TMDB API (free, for real film poster images)
|
||||
# Get your key at: https://www.themoviedb.org/settings/api
|
||||
VITE_TMDB_API_KEY=your-tmdb-key-here
|
||||
|
||||
# Development flags
|
||||
VITE_DEV_MODE=true
|
||||
VITE_MOCK_MEDIA_SOURCES=true
|
||||
VITE_DISABLE_CRYPTO=true
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# Environment (secrets)
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Tauri
|
||||
packages/app/src-tauri/target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Storybook
|
||||
storybook-static/
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "aiui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "The next-generation AI content surface UI",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter @aiui/app dev",
|
||||
"dev:core": "pnpm --filter @aiui/core dev",
|
||||
"build": "turbo build",
|
||||
"test": "turbo test",
|
||||
"lint": "turbo lint",
|
||||
"typecheck": "turbo typecheck",
|
||||
"clean": "turbo clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "latest",
|
||||
"typescript": "~5.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": ["esbuild"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
|
||||
<meta name="theme-color" content="#0a0a0a" />
|
||||
<title>AIUI</title>
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@aiui/app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "AIUI reference application",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint src/",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aiui/core": "workspace:*",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"pinia": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "latest",
|
||||
"vite": "latest",
|
||||
"vue-tsc": "latest",
|
||||
"vitest": "latest",
|
||||
"eslint": "latest",
|
||||
"typescript": "~5.8.0",
|
||||
"tailwindcss": "latest",
|
||||
"@tailwindcss/vite": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="h-dvh flex flex-col bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<object, object, unknown>
|
||||
export default component
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_OPENROUTER_API_KEY: string
|
||||
readonly VITE_TMDB_API_KEY: string
|
||||
readonly VITE_DEV_MODE: string
|
||||
readonly VITE_MOCK_MEDIA_SOURCES: string
|
||||
readonly VITE_DISABLE_CRYPTO: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import './styles/main.css'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'chat',
|
||||
component: () => import('./pages/ChatPage.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,862 @@
|
||||
import type { Film } from '@aiui/core/types/content'
|
||||
|
||||
const TMDB_IMG = 'https://image.tmdb.org/t/p'
|
||||
const POSTER = `${TMDB_IMG}/w342`
|
||||
const BACKDROP = `${TMDB_IMG}/w780`
|
||||
|
||||
export const mockFilms: Film[] = [
|
||||
{
|
||||
id: 'f1',
|
||||
title: 'Blade Runner 2049',
|
||||
year: 2017,
|
||||
posterUrl: `${POSTER}/gajva2L0rPYkEWjzgFlBXCAVBE5.jpg`,
|
||||
backdropUrl: `${BACKDROP}/sAtoMqDVhNDQBc3QJL3RF6hlhGq.jpg`,
|
||||
synopsis: 'A young blade runner discovers a long-buried secret that leads him to track down former blade runner Rick Deckard, who has been missing for thirty years.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Thriller'],
|
||||
rating: 7.5,
|
||||
runtime: 164,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Ryan Gosling', 'Harrison Ford', 'Ana de Armas'],
|
||||
trailerUrl: 'https://www.youtube.com/watch?v=gCcx85e8rTo',
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12345', quality: '4K', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Rental', url: 'https://www.youtube.com/watch?v=gCcx85e8rTo', quality: 'HD', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
title: 'Arrival',
|
||||
year: 2016,
|
||||
posterUrl: `${POSTER}/x2FJsf1ElAgr63Y3LNUTq7KZPno.jpg`,
|
||||
backdropUrl: `${BACKDROP}/yIZ1xendHwnlqSEIFg5MpkOQ1qk.jpg`,
|
||||
synopsis: 'A linguist works with the military to communicate with alien lifeforms after twelve mysterious spacecraft appear around the world.',
|
||||
genres: ['Sci-Fi', 'Drama'],
|
||||
rating: 7.9,
|
||||
runtime: 116,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Amy Adams', 'Jeremy Renner', 'Forest Whitaker'],
|
||||
trailerUrl: 'https://www.youtube.com/watch?v=tFMo3UJ4B4g',
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12346', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f3',
|
||||
title: 'Dune',
|
||||
year: 2021,
|
||||
posterUrl: `${POSTER}/d5NXSklXo0qyIYkgV94XAgMIckC.jpg`,
|
||||
backdropUrl: `${BACKDROP}/jYEW5xZkZk2WTrdbMGAPFuBqbDc.jpg`,
|
||||
synopsis: 'Paul Atreides, a brilliant and gifted young man born into a great destiny beyond his understanding, must travel to the most dangerous planet in the universe.',
|
||||
genres: ['Sci-Fi', 'Adventure', 'Drama'],
|
||||
rating: 7.8,
|
||||
runtime: 155,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Timothée Chalamet', 'Rebecca Ferguson', 'Zendaya'],
|
||||
trailerUrl: 'https://www.youtube.com/watch?v=n9xhJrPXop4',
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12347', quality: '4K HDR', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Purchase', url: 'https://www.youtube.com/watch?v=n9xhJrPXop4', quality: '4K', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f4',
|
||||
title: 'Interstellar',
|
||||
year: 2014,
|
||||
posterUrl: `${POSTER}/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg`,
|
||||
backdropUrl: `${BACKDROP}/xJHokMbljvjADYdit5fK1DDtAoB.jpg`,
|
||||
synopsis: 'A team of explorers travel through a wormhole in space in an attempt to ensure humanity\'s survival.',
|
||||
genres: ['Sci-Fi', 'Adventure', 'Drama'],
|
||||
rating: 8.6,
|
||||
runtime: 169,
|
||||
director: 'Christopher Nolan',
|
||||
cast: ['Matthew McConaughey', 'Anne Hathaway', 'Jessica Chastain'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12348', quality: '4K IMAX', icon: 'plex' },
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/abc123', quality: '1080p', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f5',
|
||||
title: 'The Matrix',
|
||||
year: 1999,
|
||||
posterUrl: `${POSTER}/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg`,
|
||||
backdropUrl: `${BACKDROP}/fNG7i7RqMErkcqhohV2a6cV1Ehy.jpg`,
|
||||
synopsis: 'A computer programmer discovers that reality as he knows it is a simulation created by machines, and joins a rebellion to break free.',
|
||||
genres: ['Sci-Fi', 'Action'],
|
||||
rating: 8.7,
|
||||
runtime: 136,
|
||||
director: 'Lana Wachowski',
|
||||
cast: ['Keanu Reeves', 'Laurence Fishburne', 'Carrie-Anne Moss'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12349', quality: '4K', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/thematrix', quality: 'SD', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f6',
|
||||
title: 'Parasite',
|
||||
year: 2019,
|
||||
posterUrl: `${POSTER}/7IiTTgloJzvGI1TAYymCfbfl3vT.jpg`,
|
||||
backdropUrl: `${BACKDROP}/TU9NIjwzjoKPwQHoHshkFcQUCG.jpg`,
|
||||
synopsis: 'Greed and class discrimination threaten the newly formed symbiotic relationship between the wealthy Park family and the destitute Kim clan.',
|
||||
genres: ['Drama', 'Thriller', 'Comedy'],
|
||||
rating: 8.5,
|
||||
runtime: 132,
|
||||
director: 'Bong Joon-ho',
|
||||
cast: ['Song Kang-ho', 'Lee Sun-kyun', 'Cho Yeo-jeong'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12350', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f7',
|
||||
title: 'Mad Max: Fury Road',
|
||||
year: 2015,
|
||||
posterUrl: `${POSTER}/8tZYtuWezp8JbcsvHYO0O46tFBO.jpg`,
|
||||
backdropUrl: `${BACKDROP}/phszHPFVhPHhMZgo0fWTKBDQsJA.jpg`,
|
||||
synopsis: 'In a post-apocalyptic wasteland, a woman rebels against a tyrannical ruler in search for her homeland with the aid of a drifter.',
|
||||
genres: ['Action', 'Adventure', 'Sci-Fi'],
|
||||
rating: 8.1,
|
||||
runtime: 120,
|
||||
director: 'George Miller',
|
||||
cast: ['Tom Hardy', 'Charlize Theron', 'Nicholas Hoult'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12351', quality: '4K HDR', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Rental', url: 'https://www.youtube.com/watch?v=hEJnMQG9ev8', quality: 'HD', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f8',
|
||||
title: 'Ex Machina',
|
||||
year: 2014,
|
||||
posterUrl: `${POSTER}/btbRB7BrD887pKiSfMXtCCHvyOo.jpg`,
|
||||
backdropUrl: `${BACKDROP}/4uOaYtBvAYXOPSHuWLpFOS17SO.jpg`,
|
||||
synopsis: 'A young programmer is selected to participate in a groundbreaking experiment in synthetic intelligence by evaluating the human qualities of a highly advanced humanoid AI.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Thriller'],
|
||||
rating: 7.7,
|
||||
runtime: 108,
|
||||
director: 'Alex Garland',
|
||||
cast: ['Alicia Vikander', 'Domhnall Gleeson', 'Oscar Isaac'],
|
||||
sources: [
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/def456', quality: '1080p', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f9',
|
||||
title: 'The Grand Budapest Hotel',
|
||||
year: 2014,
|
||||
posterUrl: `${POSTER}/eWDyJQ3yz8mvHj7pKfRPs0h2CAl.jpg`,
|
||||
backdropUrl: `${BACKDROP}/nX5XotM9yprCKarRH4fzOq1WZ5y.jpg`,
|
||||
synopsis: 'A writer encounters the owner of an aging high-class hotel, who tells him of his early years serving as a lobby boy in the hotel\'s glorious years.',
|
||||
genres: ['Comedy', 'Drama', 'Adventure'],
|
||||
rating: 8.1,
|
||||
runtime: 99,
|
||||
director: 'Wes Anderson',
|
||||
cast: ['Ralph Fiennes', 'F. Murray Abraham', 'Mathieu Amalric'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12352', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f10',
|
||||
title: 'Whiplash',
|
||||
year: 2014,
|
||||
posterUrl: `${POSTER}/7fn624j5lj3xTme2SgiLCeuedmO.jpg`,
|
||||
backdropUrl: `${BACKDROP}/fRGxZuo7jJUWQsVg9PREb98Aclp.jpg`,
|
||||
synopsis: 'A promising young drummer enrolls at a cut-throat music conservatory where his dreams of greatness are mentored by an instructor who will stop at nothing.',
|
||||
genres: ['Drama', 'Music'],
|
||||
rating: 8.5,
|
||||
runtime: 107,
|
||||
director: 'Damien Chazelle',
|
||||
cast: ['Miles Teller', 'J.K. Simmons', 'Melissa Benoist'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12353', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Tubi', url: 'https://tubitv.com/movies/whiplash', quality: 'HD', icon: 'tubi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f11',
|
||||
title: 'Drive',
|
||||
year: 2011,
|
||||
posterUrl: `${POSTER}/602vevIURmpDfzbnv5Ubi6wIkQm.jpg`,
|
||||
backdropUrl: `${BACKDROP}/wMELEDyvaJsAykFP1cDasWKlXKv.jpg`,
|
||||
synopsis: 'A mysterious Hollywood stuntman and mechanic moonlights as a getaway driver and finds himself in trouble when he helps out his neighbor.',
|
||||
genres: ['Drama', 'Crime', 'Action'],
|
||||
rating: 7.8,
|
||||
runtime: 100,
|
||||
director: 'Nicolas Winding Refn',
|
||||
cast: ['Ryan Gosling', 'Carey Mulligan', 'Bryan Cranston'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12354', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f12',
|
||||
title: 'Spirited Away',
|
||||
year: 2001,
|
||||
posterUrl: `${POSTER}/39wmItIWsg5sZMyRUHLkWBcuVCM.jpg`,
|
||||
backdropUrl: `${BACKDROP}/Ab8mkHmkYADjU7wQiOkia9BzGvS.jpg`,
|
||||
synopsis: 'During her family\'s move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods, witches, and spirits.',
|
||||
genres: ['Animation', 'Fantasy', 'Adventure'],
|
||||
rating: 8.6,
|
||||
runtime: 125,
|
||||
director: 'Hayao Miyazaki',
|
||||
cast: ['Rumi Hiiragi', 'Miyu Irino', 'Mari Natsuki'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12355', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/ghi789', quality: '1080p', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f13',
|
||||
title: 'The Social Network',
|
||||
year: 2010,
|
||||
posterUrl: `${POSTER}/n0ybibhJtQ5icDqTp8eRytcIHJx.jpg`,
|
||||
backdropUrl: `${BACKDROP}/yyMQLz9pMzB7LbLNz4l8Y1KLLBZ.jpg`,
|
||||
synopsis: 'As Harvard student Mark Zuckerberg creates the social networking site that would become known as Facebook, he is sued by the twins who claimed he stole their idea.',
|
||||
genres: ['Drama', 'Biography'],
|
||||
rating: 7.7,
|
||||
runtime: 120,
|
||||
director: 'David Fincher',
|
||||
cast: ['Jesse Eisenberg', 'Andrew Garfield', 'Justin Timberlake'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12356', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f14',
|
||||
title: 'Her',
|
||||
year: 2013,
|
||||
posterUrl: `${POSTER}/eCOtqtfvn7mxGl6nfmq4b1exJRc.jpg`,
|
||||
backdropUrl: `${BACKDROP}/bbS05YfasBhMsQqY1A7gKjETJNu.jpg`,
|
||||
synopsis: 'In a near future, a lonely writer develops an unlikely relationship with an operating system designed to meet his every need.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Romance'],
|
||||
rating: 8.0,
|
||||
runtime: 126,
|
||||
director: 'Spike Jonze',
|
||||
cast: ['Joaquin Phoenix', 'Scarlett Johansson', 'Amy Adams'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12357', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f15',
|
||||
title: 'Moonlight',
|
||||
year: 2016,
|
||||
posterUrl: `${POSTER}/4911T5FbJ9eD2Faz5Z8cT3SUhU3.jpg`,
|
||||
backdropUrl: `${BACKDROP}/A52Lnk3UkxF4Lg3LkkmGNBCnEQ.jpg`,
|
||||
synopsis: 'A young African-American man grapples with his identity and sexuality while experiencing the everyday struggles of childhood, adolescence, and burgeoning adulthood.',
|
||||
genres: ['Drama'],
|
||||
rating: 7.4,
|
||||
runtime: 111,
|
||||
director: 'Barry Jenkins',
|
||||
cast: ['Mahershala Ali', 'Naomie Harris', 'Trevante Rhodes'],
|
||||
sources: [
|
||||
{ type: 'free-web', name: 'Tubi', url: 'https://tubitv.com/movies/moonlight', quality: 'HD', icon: 'tubi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f16',
|
||||
title: 'No Country for Old Men',
|
||||
year: 2007,
|
||||
posterUrl: `${POSTER}/bj1v6YKF8yHqA489GFfPC8oKjAz.jpg`,
|
||||
backdropUrl: `${BACKDROP}/AoJn0YDsrjUHqBLqmJmqDTz8k6y.jpg`,
|
||||
synopsis: 'Violence and mayhem ensue after a hunter stumbles upon a drug deal gone wrong and more than two million dollars in cash near the Rio Grande.',
|
||||
genres: ['Crime', 'Drama', 'Thriller'],
|
||||
rating: 8.1,
|
||||
runtime: 122,
|
||||
director: 'Joel Coen',
|
||||
cast: ['Tommy Lee Jones', 'Javier Bardem', 'Josh Brolin'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12358', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f17',
|
||||
title: 'Everything Everywhere All at Once',
|
||||
year: 2022,
|
||||
posterUrl: `${POSTER}/w3LxiVYdWWRvEVdn5RYq6jIqkb1.jpg`,
|
||||
backdropUrl: `${BACKDROP}/wY1HZjM2GvHH4aSyrDvEfVEEW4A.jpg`,
|
||||
synopsis: 'An aging Chinese immigrant is swept up in an insane adventure, where she alone can save what\'s important to her by connecting with the lives she could have led.',
|
||||
genres: ['Action', 'Adventure', 'Comedy'],
|
||||
rating: 7.8,
|
||||
runtime: 139,
|
||||
director: 'Daniel Kwan',
|
||||
cast: ['Michelle Yeoh', 'Ke Huy Quan', 'Stephanie Hsu'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12359', quality: '4K', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Purchase', url: 'https://www.youtube.com/watch?v=wxN1T1qdQ', quality: '4K', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f18',
|
||||
title: 'The Shawshank Redemption',
|
||||
year: 1994,
|
||||
posterUrl: `${POSTER}/q6y0Go1tsGEsmtFryDOJo3dEmqu.jpg`,
|
||||
backdropUrl: `${BACKDROP}/kXfqcdQKsToO0OUXHcrrNCHDBzO.jpg`,
|
||||
synopsis: 'Over the course of several years, two convicts form a friendship, seeking consolation and, eventually, redemption through basic compassion.',
|
||||
genres: ['Drama'],
|
||||
rating: 9.3,
|
||||
runtime: 142,
|
||||
director: 'Frank Darabont',
|
||||
cast: ['Tim Robbins', 'Morgan Freeman', 'Bob Gunton'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12360', quality: '4K', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/shawshank', quality: 'SD', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f19',
|
||||
title: 'Sicario',
|
||||
year: 2015,
|
||||
posterUrl: `${POSTER}/z8sJM6ijaEmVDB8Uo3NJNOoXNqV.jpg`,
|
||||
backdropUrl: `${BACKDROP}/bLxkraPUhKlE7JQBfaWbWg2U70b.jpg`,
|
||||
synopsis: 'An idealistic FBI agent is enlisted by a government task force to aid in the escalating war against drugs at the border area between the U.S. and Mexico.',
|
||||
genres: ['Action', 'Crime', 'Drama'],
|
||||
rating: 7.6,
|
||||
runtime: 121,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Emily Blunt', 'Josh Brolin', 'Benicio del Toro'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12361', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f20',
|
||||
title: 'The Dark Knight',
|
||||
year: 2008,
|
||||
posterUrl: `${POSTER}/qJ2tW6WMUDux911kpWYrhaCj5l8.jpg`,
|
||||
backdropUrl: `${BACKDROP}/nMKdUUepR0i5zn0y1T4CsSB5ez.jpg`,
|
||||
synopsis: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.',
|
||||
genres: ['Action', 'Crime', 'Drama'],
|
||||
rating: 9.0,
|
||||
runtime: 152,
|
||||
director: 'Christopher Nolan',
|
||||
cast: ['Christian Bale', 'Heath Ledger', 'Aaron Eckhart'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12362', quality: '4K IMAX', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Rental', url: 'https://www.youtube.com/watch?v=EXeTwQWrcwY', quality: 'HD', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f21',
|
||||
title: 'Inception',
|
||||
year: 2010,
|
||||
posterUrl: `${POSTER}/edv5CZvWj09upOsy2Y6IwDhK8bt.jpg`,
|
||||
backdropUrl: `${BACKDROP}/s3TBrRGB1iav7gFOCNx3H31MoES.jpg`,
|
||||
synopsis: 'A thief who steals corporate secrets through dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.',
|
||||
genres: ['Sci-Fi', 'Action', 'Thriller'],
|
||||
rating: 8.8,
|
||||
runtime: 148,
|
||||
director: 'Christopher Nolan',
|
||||
cast: ['Leonardo DiCaprio', 'Joseph Gordon-Levitt', 'Elliot Page'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12363', quality: '4K', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f22',
|
||||
title: 'Hereditary',
|
||||
year: 2018,
|
||||
posterUrl: `${POSTER}/p9fmuz2Oj3FtMGSbVMBCabd06VO.jpg`,
|
||||
backdropUrl: `${BACKDROP}/5GbkXg1e3i4X2Gfyg4c8JdwVYmt.jpg`,
|
||||
synopsis: 'A grieving family is haunted by tragic and disturbing occurrences after the death of their secretive grandmother.',
|
||||
genres: ['Horror', 'Drama', 'Mystery'],
|
||||
rating: 7.3,
|
||||
runtime: 127,
|
||||
director: 'Ari Aster',
|
||||
cast: ['Toni Collette', 'Milly Shapiro', 'Gabriel Byrne'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12364', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f23',
|
||||
title: 'The Lighthouse',
|
||||
year: 2019,
|
||||
posterUrl: `${POSTER}/3nBrkqpG4Mzp7BnsLWDaiXH3VJZ.jpg`,
|
||||
backdropUrl: `${BACKDROP}/5BkSkNbcABByJ6MFIxQxKiuN7UI.jpg`,
|
||||
synopsis: 'Two lighthouse keepers try to maintain their sanity while living on a remote and mysterious New England island in the 1890s.',
|
||||
genres: ['Drama', 'Fantasy', 'Horror'],
|
||||
rating: 7.5,
|
||||
runtime: 109,
|
||||
director: 'Robert Eggers',
|
||||
cast: ['Willem Dafoe', 'Robert Pattinson'],
|
||||
sources: [
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/jkl012', quality: '1080p', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f24',
|
||||
title: 'Akira',
|
||||
year: 1988,
|
||||
posterUrl: `${POSTER}/neZ0ykEsPqxamsX6o5QNUFILQpa.jpg`,
|
||||
backdropUrl: `${BACKDROP}/qeyColorZbMBevU5uMZDMXGxiB7.jpg`,
|
||||
synopsis: 'A secret military project endangers Neo-Tokyo when it turns a biker gang member into a rampaging psychic psychopath.',
|
||||
genres: ['Animation', 'Sci-Fi', 'Action'],
|
||||
rating: 8.0,
|
||||
runtime: 124,
|
||||
director: 'Katsuhiro Otomo',
|
||||
cast: ['Mitsuo Iwata', 'Nozomu Sasaki', 'Mami Koyama'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12365', quality: '4K', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/akira', quality: 'SD', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f25',
|
||||
title: 'There Will Be Blood',
|
||||
year: 2007,
|
||||
posterUrl: `${POSTER}/fa0RDkAlCec0STeMNAhPaF89q6U.jpg`,
|
||||
backdropUrl: `${BACKDROP}/y6lFCjxEGJ4TpQRa5VHRuA6K0M.jpg`,
|
||||
synopsis: 'A story of family, religion, hatred, oil and madness, focusing on a turn-of-the-century prospector in the early days of the business.',
|
||||
genres: ['Drama'],
|
||||
rating: 8.2,
|
||||
runtime: 158,
|
||||
director: 'Paul Thomas Anderson',
|
||||
cast: ['Daniel Day-Lewis', 'Paul Dano', 'Ciarán Hinds'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12366', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f26',
|
||||
title: 'Oldboy',
|
||||
year: 2003,
|
||||
posterUrl: `${POSTER}/pWDtjs568ZfOTMbURQBYuT4Qxka.jpg`,
|
||||
backdropUrl: `${BACKDROP}/4CnTFELRf0VN1jp59iFmFpYc1S.jpg`,
|
||||
synopsis: 'After being kidnapped and imprisoned for fifteen years, Oh Dae-Su is released, only to find that he must find his captor in five days.',
|
||||
genres: ['Action', 'Drama', 'Mystery'],
|
||||
rating: 8.4,
|
||||
runtime: 120,
|
||||
director: 'Park Chan-wook',
|
||||
cast: ['Choi Min-sik', 'Yoo Ji-tae', 'Kang Hye-jung'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12367', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f27',
|
||||
title: 'Stalker',
|
||||
year: 1979,
|
||||
posterUrl: `${POSTER}/iPbNAc2GnqZgKpAAbEE0Uy9V1Db.jpg`,
|
||||
backdropUrl: `${BACKDROP}/sBPkb2gPXJrBWa60fAdh8vj4M3I.jpg`,
|
||||
synopsis: 'A guide leads two men through an area known as the Zone to find a room that grants wishes.',
|
||||
genres: ['Sci-Fi', 'Drama'],
|
||||
rating: 8.2,
|
||||
runtime: 163,
|
||||
director: 'Andrei Tarkovsky',
|
||||
cast: ['Aleksandr Kaydanovskiy', 'Anatoliy Solonitsyn', 'Nikolay Grinko'],
|
||||
sources: [
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/stalker-1979', quality: 'HD Restored', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f28',
|
||||
title: 'Amélie',
|
||||
year: 2001,
|
||||
posterUrl: `${POSTER}/nSxDa3M9aMvs3DqxMkNizqRKvEa.jpg`,
|
||||
backdropUrl: `${BACKDROP}/6eJMISgddTe8i4MH4CnPaFcM3hM.jpg`,
|
||||
synopsis: 'Amélie is an innocent and naive girl in Paris with her own sense of justice. She decides to help those around her and, along the way, discovers love.',
|
||||
genres: ['Comedy', 'Romance'],
|
||||
rating: 8.3,
|
||||
runtime: 122,
|
||||
director: 'Jean-Pierre Jeunet',
|
||||
cast: ['Audrey Tautou', 'Mathieu Kassovitz', 'Rufus'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12368', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f29',
|
||||
title: 'In the Mood for Love',
|
||||
year: 2000,
|
||||
posterUrl: `${POSTER}/iYypPT4bhqXfq1b6sFnVVR4FySt.jpg`,
|
||||
backdropUrl: `${BACKDROP}/ArbbMEiGPbV4g3a6ahCfrR8pF6a.jpg`,
|
||||
synopsis: 'Two neighbors form a strong bond after both suspect extramarital activities of their spouses. However, they agree to keep their relationship platonic.',
|
||||
genres: ['Drama', 'Romance'],
|
||||
rating: 8.1,
|
||||
runtime: 98,
|
||||
director: 'Wong Kar-wai',
|
||||
cast: ['Tony Leung Chiu-wai', 'Maggie Cheung', 'Ping Lam Siu'],
|
||||
sources: [
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/mno345', quality: '4K Restored', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f30',
|
||||
title: 'The Godfather',
|
||||
year: 1972,
|
||||
posterUrl: `${POSTER}/3bhkrj58Vtu7enYsRolD1fZdja1.jpg`,
|
||||
backdropUrl: `${BACKDROP}/tmU7GeKVybMWFButWEGl2M4GeiP.jpg`,
|
||||
synopsis: 'The aging patriarch of an organized crime dynasty in postwar New York City transfers control of his clandestine empire to his reluctant youngest son.',
|
||||
genres: ['Crime', 'Drama'],
|
||||
rating: 9.2,
|
||||
runtime: 175,
|
||||
director: 'Francis Ford Coppola',
|
||||
cast: ['Marlon Brando', 'Al Pacino', 'James Caan'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12369', quality: '4K Restored', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Purchase', url: 'https://www.youtube.com/watch?v=UaVTIH8mujA', quality: '4K', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f31',
|
||||
title: '2001: A Space Odyssey',
|
||||
year: 1968,
|
||||
posterUrl: `${POSTER}/ve72VzNqjgM69Ml8om3OyoEQpFe.jpg`,
|
||||
backdropUrl: `${BACKDROP}/hxl1y0AFBbzLXFHMDFcqSMsq3qz.jpg`,
|
||||
synopsis: 'After uncovering a mysterious artifact buried beneath the lunar surface, a spacecraft is sent to Jupiter to find its origins.',
|
||||
genres: ['Sci-Fi', 'Adventure'],
|
||||
rating: 8.3,
|
||||
runtime: 149,
|
||||
director: 'Stanley Kubrick',
|
||||
cast: ['Keir Dullea', 'Gary Lockwood', 'William Sylvester'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12370', quality: '4K IMAX', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f32',
|
||||
title: 'Pulp Fiction',
|
||||
year: 1994,
|
||||
posterUrl: `${POSTER}/d5iIlFn5s0ImszYzBPb8JPIfbXD.jpg`,
|
||||
backdropUrl: `${BACKDROP}/suaEOtk1N1sgg2MTM7oZd2cfVp3.jpg`,
|
||||
synopsis: 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.',
|
||||
genres: ['Crime', 'Drama', 'Thriller'],
|
||||
rating: 8.9,
|
||||
runtime: 154,
|
||||
director: 'Quentin Tarantino',
|
||||
cast: ['John Travolta', 'Samuel L. Jackson', 'Uma Thurman'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12371', quality: '4K', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Tubi', url: 'https://tubitv.com/movies/pulp-fiction', quality: 'HD', icon: 'tubi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f33',
|
||||
title: 'Annihilation',
|
||||
year: 2018,
|
||||
posterUrl: `${POSTER}/d3qcpfNwbAMCNqWDHzPQsUoj7ig.jpg`,
|
||||
backdropUrl: `${BACKDROP}/1Y2YzOmVhCQ4c0TjlCqfJBl8Ykz.jpg`,
|
||||
synopsis: 'A biologist signs up for a dangerous, secret expedition into a mysterious zone where the laws of nature don\'t apply.',
|
||||
genres: ['Sci-Fi', 'Horror', 'Adventure'],
|
||||
rating: 6.8,
|
||||
runtime: 115,
|
||||
director: 'Alex Garland',
|
||||
cast: ['Natalie Portman', 'Jennifer Jason Leigh', 'Tessa Thompson'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12372', quality: '4K', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f34',
|
||||
title: 'Children of Men',
|
||||
year: 2006,
|
||||
posterUrl: `${POSTER}/uONhGnVGieGqjw74vRIh5DLaZbr.jpg`,
|
||||
backdropUrl: `${BACKDROP}/cM7wqMsIEjMZ0qsIXRoYZqfk0s.jpg`,
|
||||
synopsis: 'In 2027, in a chaotic world in which women have somehow become infertile, a former activist agrees to help transport a miraculously pregnant woman to a sanctuary at sea.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Thriller'],
|
||||
rating: 7.9,
|
||||
runtime: 109,
|
||||
director: 'Alfonso Cuarón',
|
||||
cast: ['Clive Owen', 'Julianne Moore', 'Michael Caine'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12373', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/pqr678', quality: '1080p', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f35',
|
||||
title: 'Pan\'s Labyrinth',
|
||||
year: 2006,
|
||||
posterUrl: `${POSTER}/s2Ih5jSFCJwMqEIVsBYyNn0Tyx5.jpg`,
|
||||
backdropUrl: `${BACKDROP}/4qfadVQIJm7R3LzqObDtBGN1OXi.jpg`,
|
||||
synopsis: 'In the Falangist Spain of 1944, the bookish young stepdaughter of a sadistic army officer escapes into an eerie but captivating fantasy world.',
|
||||
genres: ['Drama', 'Fantasy', 'War'],
|
||||
rating: 8.2,
|
||||
runtime: 118,
|
||||
director: 'Guillermo del Toro',
|
||||
cast: ['Ivana Baquero', 'Ariadna Gil', 'Sergi López'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12374', quality: '4K', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f36',
|
||||
title: 'Eternal Sunshine of the Spotless Mind',
|
||||
year: 2004,
|
||||
posterUrl: `${POSTER}/5MwkWH9tYHv3mV9OdYTMR5qreIz.jpg`,
|
||||
backdropUrl: `${BACKDROP}/6f1a2p19dOLF0USwPgHx3TXbD8U.jpg`,
|
||||
synopsis: 'When their relationship turns sour, a couple undergoes a medical procedure to have each other erased from their memories.',
|
||||
genres: ['Drama', 'Romance', 'Sci-Fi'],
|
||||
rating: 8.3,
|
||||
runtime: 108,
|
||||
director: 'Michel Gondry',
|
||||
cast: ['Jim Carrey', 'Kate Winslet', 'Tom Wilkinson'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12375', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f37',
|
||||
title: 'Taxi Driver',
|
||||
year: 1976,
|
||||
posterUrl: `${POSTER}/ekstpH614fwDX8DUln1a2Opz0N8.jpg`,
|
||||
backdropUrl: `${BACKDROP}/ghbSODMHaSDNkGg2yF6R4Rq2FM.jpg`,
|
||||
synopsis: 'A mentally unstable veteran works as a nighttime taxi driver in New York City, where the perceived decadence and sleaze fuels his urge for violent action.',
|
||||
genres: ['Crime', 'Drama'],
|
||||
rating: 8.2,
|
||||
runtime: 114,
|
||||
director: 'Martin Scorsese',
|
||||
cast: ['Robert De Niro', 'Jodie Foster', 'Cybill Shepherd'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12376', quality: '4K Restored', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f38',
|
||||
title: 'Mulholland Drive',
|
||||
year: 2001,
|
||||
posterUrl: `${POSTER}/tVxGt7uffLVhIIcwuldXjy4JvMH.jpg`,
|
||||
backdropUrl: `${BACKDROP}/bV8PLdElQVafeXvOnnFiFZNrFPM.jpg`,
|
||||
synopsis: 'After a car wreck on the winding Mulholland Drive renders a woman amnesiac, she and a perky Hollywood-hopeful search for clues and answers.',
|
||||
genres: ['Drama', 'Mystery', 'Thriller'],
|
||||
rating: 7.9,
|
||||
runtime: 147,
|
||||
director: 'David Lynch',
|
||||
cast: ['Naomi Watts', 'Laura Harring', 'Justin Theroux'],
|
||||
sources: [
|
||||
{ type: 'nextcloud', name: 'Nextcloud Files', url: 'https://cloud.example.com/s/stu901', quality: '4K Restored', icon: 'nextcloud' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f39',
|
||||
title: 'The Departed',
|
||||
year: 2006,
|
||||
posterUrl: `${POSTER}/nT97ifVT2J1yMQmeq20Dqv28.jpg`,
|
||||
backdropUrl: `${BACKDROP}/8Id2Z4LB12BMLMoJnFHMFht4bXP.jpg`,
|
||||
synopsis: 'An undercover cop and a mole in the police attempt to identify each other while infiltrating an Irish gang in South Boston.',
|
||||
genres: ['Crime', 'Drama', 'Thriller'],
|
||||
rating: 8.5,
|
||||
runtime: 151,
|
||||
director: 'Martin Scorsese',
|
||||
cast: ['Leonardo DiCaprio', 'Matt Damon', 'Jack Nicholson'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12377', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f40',
|
||||
title: 'Prisoners',
|
||||
year: 2013,
|
||||
posterUrl: `${POSTER}/uhBqwitKfOP0FBPblUBqxgce6hg.jpg`,
|
||||
backdropUrl: `${BACKDROP}/2E0PFGHgJBMSJ4GpjBmCnaBqCmz.jpg`,
|
||||
synopsis: 'When Keller Dover\'s daughter and her friend go missing, he takes matters into his own hands as the police pursue multiple leads.',
|
||||
genres: ['Crime', 'Drama', 'Mystery'],
|
||||
rating: 8.1,
|
||||
runtime: 153,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Hugh Jackman', 'Jake Gyllenhaal', 'Viola Davis'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12378', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Rental', url: 'https://www.youtube.com/watch?v=bLv3JM2x2bc', quality: 'HD', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f41',
|
||||
title: 'Goodfellas',
|
||||
year: 1990,
|
||||
posterUrl: `${POSTER}/aKuFiU82s5ISJpGZp7YkIr3kCUd.jpg`,
|
||||
backdropUrl: `${BACKDROP}/sw7mordbZxgITU877yTpZCud90M.jpg`,
|
||||
synopsis: 'The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners.',
|
||||
genres: ['Crime', 'Drama', 'Biography'],
|
||||
rating: 8.7,
|
||||
runtime: 146,
|
||||
director: 'Martin Scorsese',
|
||||
cast: ['Robert De Niro', 'Ray Liotta', 'Joe Pesci'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12379', quality: '4K', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f42',
|
||||
title: 'Ghost in the Shell',
|
||||
year: 1995,
|
||||
posterUrl: `${POSTER}/9gC88zYUBbuGRzgyNcE1xj0QRZL.jpg`,
|
||||
backdropUrl: `${BACKDROP}/t7Gy2V5tGGWsRCsH7m6hI1OXm06.jpg`,
|
||||
synopsis: 'A cyborg policewoman and her partner hunt a mysterious and powerful hacker called the Puppet Master.',
|
||||
genres: ['Animation', 'Sci-Fi', 'Action'],
|
||||
rating: 8.0,
|
||||
runtime: 83,
|
||||
director: 'Mamoru Oshii',
|
||||
cast: ['Atsuko Tanaka', 'Akio Ōtsuka', 'Iemasa Kayumi'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12380', quality: '4K', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/gits1995', quality: 'SD', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f43',
|
||||
title: 'The Prestige',
|
||||
year: 2006,
|
||||
posterUrl: `${POSTER}/tRNlZbgNCNOpLpbPEz5L8G8A0JN.jpg`,
|
||||
backdropUrl: `${BACKDROP}/s0EfkKJONLVCT9Q3UrPW0q07rT1.jpg`,
|
||||
synopsis: 'After a tragic accident, two stage magicians in 1890s London engage in a battle to create the ultimate illusion while sacrificing everything they have.',
|
||||
genres: ['Drama', 'Mystery', 'Sci-Fi'],
|
||||
rating: 8.5,
|
||||
runtime: 130,
|
||||
director: 'Christopher Nolan',
|
||||
cast: ['Christian Bale', 'Hugh Jackman', 'Scarlett Johansson'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12381', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f44',
|
||||
title: 'Solaris',
|
||||
year: 1972,
|
||||
posterUrl: `${POSTER}/2sF5EfIwZMY0eT0U0iH5A43OLK2.jpg`,
|
||||
backdropUrl: `${BACKDROP}/ixjQv30AYFXxGE7RaReGJn0YnQJ.jpg`,
|
||||
synopsis: 'A psychologist is sent to a station orbiting a distant planet in order to discover what has caused the crew to go insane.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Mystery'],
|
||||
rating: 8.1,
|
||||
runtime: 167,
|
||||
director: 'Andrei Tarkovsky',
|
||||
cast: ['Natalya Bondarchuk', 'Donatas Banionis', 'Jüri Järvet'],
|
||||
sources: [
|
||||
{ type: 'free-web', name: 'Archive.org', url: 'https://archive.org/details/solaris-1972', quality: 'HD Restored', icon: 'archive' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f45',
|
||||
title: 'City of God',
|
||||
year: 2002,
|
||||
posterUrl: `${POSTER}/k7eYdWvhYQyRQoU2TB2A2Xu2TIM.jpg`,
|
||||
backdropUrl: `${BACKDROP}/efnAMhQJMH4EvEZqPEB5SRXWihi.jpg`,
|
||||
synopsis: 'In the slums of Rio, two kids\' paths diverge as one struggles to become a photographer and the other a kingpin.',
|
||||
genres: ['Crime', 'Drama'],
|
||||
rating: 8.6,
|
||||
runtime: 130,
|
||||
director: 'Fernando Meirelles',
|
||||
cast: ['Alexandre Rodrigues', 'Leandro Firmino', 'Matheus Nachtergaele'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12382', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f46',
|
||||
title: 'The Truman Show',
|
||||
year: 1998,
|
||||
posterUrl: `${POSTER}/vuza0WqY239yBXOadKlGwJsZJFE.jpg`,
|
||||
backdropUrl: `${BACKDROP}/Al5GPz9U2mB2OPktKEIiHK7v97E.jpg`,
|
||||
synopsis: 'An insurance salesman discovers his whole life is actually a reality TV show.',
|
||||
genres: ['Comedy', 'Drama', 'Sci-Fi'],
|
||||
rating: 8.2,
|
||||
runtime: 103,
|
||||
director: 'Peter Weir',
|
||||
cast: ['Jim Carrey', 'Ed Harris', 'Laura Linney'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12383', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Rental', url: 'https://www.youtube.com/watch?v=dlnmQbPGuls', quality: 'HD', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f47',
|
||||
title: 'Gattaca',
|
||||
year: 1997,
|
||||
posterUrl: `${POSTER}/rkgZpFhI4xGSWljCxPWkb1XMoB2.jpg`,
|
||||
backdropUrl: `${BACKDROP}/3oTf3cfrTJbVW3fnSQNaJZBL2NQ.jpg`,
|
||||
synopsis: 'A genetically inferior man assumes the identity of a superior one in order to pursue his lifelong dream of space travel.',
|
||||
genres: ['Sci-Fi', 'Drama', 'Thriller'],
|
||||
rating: 7.8,
|
||||
runtime: 106,
|
||||
director: 'Andrew Niccol',
|
||||
cast: ['Ethan Hawke', 'Uma Thurman', 'Jude Law'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12384', quality: '1080p', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f48',
|
||||
title: 'Memento',
|
||||
year: 2000,
|
||||
posterUrl: `${POSTER}/yuNs09hvpHVU1cBTCAk9zxsL2oW.jpg`,
|
||||
backdropUrl: `${BACKDROP}/rpMn6Rl6IrvnyXcLPLEXaJPFvK8.jpg`,
|
||||
synopsis: 'A man with short-term memory loss attempts to track down his wife\'s murderer using tattoos and notes.',
|
||||
genres: ['Mystery', 'Thriller'],
|
||||
rating: 8.4,
|
||||
runtime: 113,
|
||||
director: 'Christopher Nolan',
|
||||
cast: ['Guy Pearce', 'Carrie-Anne Moss', 'Joe Pantoliano'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12385', quality: '1080p', icon: 'plex' },
|
||||
{ type: 'free-web', name: 'Tubi', url: 'https://tubitv.com/movies/memento', quality: 'HD', icon: 'tubi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f49',
|
||||
title: 'The Thing',
|
||||
year: 1982,
|
||||
posterUrl: `${POSTER}/tzGY49kseSE9QAKk47uuDGwnSCu.jpg`,
|
||||
backdropUrl: `${BACKDROP}/hILmnSocNhXYn7mPGvn1ICeu5nq.jpg`,
|
||||
synopsis: 'A research team in Antarctica is hunted by a shape-shifting alien that assumes the appearance of its victims.',
|
||||
genres: ['Horror', 'Sci-Fi', 'Mystery'],
|
||||
rating: 8.2,
|
||||
runtime: 109,
|
||||
director: 'John Carpenter',
|
||||
cast: ['Kurt Russell', 'Wilford Brimley', 'Keith David'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12386', quality: '4K', icon: 'plex' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'f50',
|
||||
title: 'Dune: Part Two',
|
||||
year: 2024,
|
||||
posterUrl: `${POSTER}/8b8R8l88Qje9dn9OE8PY05Nxl1X.jpg`,
|
||||
backdropUrl: `${BACKDROP}/xOMo8BRK7PfcJv9JCnx7s5hj0PX.jpg`,
|
||||
synopsis: 'Paul Atreides unites with Chani and the Fremen while on a warpath of revenge against the conspirators who destroyed his family.',
|
||||
genres: ['Sci-Fi', 'Adventure', 'Drama'],
|
||||
rating: 8.3,
|
||||
runtime: 166,
|
||||
director: 'Denis Villeneuve',
|
||||
cast: ['Timothée Chalamet', 'Zendaya', 'Austin Butler'],
|
||||
sources: [
|
||||
{ type: 'plex', name: 'Plex Library', url: 'plex://play?key=/library/metadata/12387', quality: '4K IMAX', icon: 'plex' },
|
||||
{ type: 'youtube', name: 'YouTube Purchase', url: 'https://www.youtube.com/watch?v=Way9Dexny3w', quality: '4K', icon: 'youtube' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const allGenres = [...new Set(mockFilms.flatMap((f) => f.genres))].sort()
|
||||
|
||||
export const allSources = [...new Set(mockFilms.flatMap((f) => f.sources.map((s) => s.type)))]
|
||||
|
||||
export function searchFilms(query: string): Film[] {
|
||||
const q = query.toLowerCase()
|
||||
return mockFilms.filter(
|
||||
(f) =>
|
||||
f.title.toLowerCase().includes(q) ||
|
||||
f.director.toLowerCase().includes(q) ||
|
||||
f.genres.some((g) => g.toLowerCase().includes(q)) ||
|
||||
f.cast.some((c) => c.toLowerCase().includes(q))
|
||||
)
|
||||
}
|
||||
|
||||
export function filterFilms(options: {
|
||||
genres?: string[]
|
||||
minRating?: number
|
||||
sources?: string[]
|
||||
yearRange?: [number, number]
|
||||
}): Film[] {
|
||||
return mockFilms.filter((f) => {
|
||||
if (options.genres?.length && !options.genres.some((g) => f.genres.includes(g))) return false
|
||||
if (options.minRating && f.rating < options.minRating) return false
|
||||
if (options.sources?.length && !options.sources.some((s) => f.sources.some((fs) => fs.type === s))) return false
|
||||
if (options.yearRange) {
|
||||
if (f.year < options.yearRange[0] || f.year > options.yearRange[1]) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<header class="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<h1 class="text-lg font-semibold">AIUI</h1>
|
||||
<span class="text-xs text-gray-400">Phase 0 — Foundation</span>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="max-w-3xl mx-auto space-y-4">
|
||||
<p class="text-sm text-gray-500 text-center py-12">
|
||||
Start a conversation. Ask about films, code, or anything else.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-800 p-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div
|
||||
class="flex items-end gap-2 rounded-2xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-colors"
|
||||
>
|
||||
<textarea
|
||||
rows="1"
|
||||
placeholder="Message AIUI..."
|
||||
class="flex-1 resize-none bg-transparent text-base outline-none placeholder:text-gray-400 min-h-[24px] max-h-[200px]"
|
||||
/>
|
||||
<button
|
||||
class="shrink-0 w-8 h-8 flex items-center justify-center rounded-lg bg-primary text-white hover:bg-primary-dark transition-colors"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7-7l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary: #606060;
|
||||
--color-primary-light: #808080;
|
||||
--color-primary-dark: #404040;
|
||||
|
||||
--color-accent: #F7931A;
|
||||
--color-accent-hover: #E88410;
|
||||
|
||||
--color-success: #10B981;
|
||||
--color-error: #EF4444;
|
||||
--color-warning: #F59E0B;
|
||||
--color-info: #3B82F6;
|
||||
|
||||
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@aiui/core": ["../core/src"],
|
||||
"@aiui/core/*": ["../core/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"],
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@aiui/core': resolve(__dirname, '../core/src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
open: true,
|
||||
},
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+7
@@ -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
|
||||
}
|
||||
@@ -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 { 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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Generated
+2494
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "packages/plugins/*"
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@aiui/core": ["./packages/core/src"],
|
||||
"@aiui/core/*": ["./packages/core/src/*"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "./packages/core" },
|
||||
{ "path": "./packages/app" }
|
||||
]
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["build"]
|
||||
},
|
||||
"lint": {},
|
||||
"typecheck": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"clean": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user