mirror of
https://github.com/kennethnym/aris.git
synced 2026-04-13 21:31:18 +01:00
Compare commits
3 Commits
fix/unifie
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c95c730533 | |||
| 62c8dfe0b1 | |||
| e54c5d5462 |
@@ -8,5 +8,5 @@
|
|||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"newlinesBetween": true
|
"newlinesBetween": true
|
||||||
},
|
},
|
||||||
"ignorePatterns": [".claude", "fixtures"]
|
"ignorePatterns": [".claude", ".ona", "drizzle", "fixtures"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from "hono"
|
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Hono } from "hono"
|
||||||
|
|
||||||
import type { Auth } from "./index.ts"
|
import type { Auth } from "./index.ts"
|
||||||
import type { AuthSession, AuthUser } from "./session.ts"
|
import type { AuthSession, AuthUser } from "./session.ts"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import type { PgDatabase } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
import { SQL } from "bun"
|
import { SQL } from "bun"
|
||||||
import { drizzle, type BunSQLDatabase } from "drizzle-orm/bun-sql"
|
import { drizzle, type BunSQLQueryResultHKT } from "drizzle-orm/bun-sql"
|
||||||
|
|
||||||
import * as schema from "./schema.ts"
|
import * as schema from "./schema.ts"
|
||||||
|
|
||||||
export type Database = BunSQLDatabase<typeof schema>
|
/** Covers both the top-level drizzle instance and transaction handles. */
|
||||||
|
export type Database = PgDatabase<BunSQLQueryResultHKT, typeof schema>
|
||||||
|
|
||||||
export interface DatabaseConnection {
|
export interface DatabaseConnection {
|
||||||
db: Database
|
db: Database
|
||||||
|
|||||||
@@ -47,5 +47,3 @@ export function createFeedEnhancer(config: FeedEnhancerConfig): FeedEnhancer {
|
|||||||
return mergeEnhancement(items, result, currentTime)
|
return mergeEnhancement(items, result, currentTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export function buildPrompt(
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const hasUnfilledSlots =
|
const hasUnfilledSlots =
|
||||||
item.slots &&
|
item.slots && Object.values(item.slots).some((slot) => slot.content === null)
|
||||||
Object.values(item.slots).some((slot) => slot.content === null)
|
|
||||||
|
|
||||||
if (hasUnfilledSlots) {
|
if (hasUnfilledSlots) {
|
||||||
enhanceItems.push({
|
enhanceItems.push({
|
||||||
@@ -79,9 +78,7 @@ export function buildPrompt(
|
|||||||
*/
|
*/
|
||||||
export function hasUnfilledSlots(items: FeedItem[]): boolean {
|
export function hasUnfilledSlots(items: FeedItem[]): boolean {
|
||||||
return items.some(
|
return items.some(
|
||||||
(item) =>
|
(item) => item.slots && Object.values(item.slots).some((slot) => slot.content === null),
|
||||||
item.slots &&
|
|
||||||
Object.values(item.slots).some((slot) => slot.content === null),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +126,20 @@ function extractCalendarEntry(item: FeedItem): CalendarEntry | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const
|
||||||
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const
|
const MONTHS = [
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
] as const
|
||||||
|
|
||||||
function pad2(n: number): string {
|
function pad2(n: number): string {
|
||||||
return n.toString().padStart(2, "0")
|
return n.toString().padStart(2, "0")
|
||||||
@@ -144,7 +154,11 @@ function formatDayShort(date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDayLabel(date: Date, currentTime: Date): string {
|
function formatDayLabel(date: Date, currentTime: Date): string {
|
||||||
const currentDay = Date.UTC(currentTime.getUTCFullYear(), currentTime.getUTCMonth(), currentTime.getUTCDate())
|
const currentDay = Date.UTC(
|
||||||
|
currentTime.getUTCFullYear(),
|
||||||
|
currentTime.getUTCMonth(),
|
||||||
|
currentTime.getUTCDate(),
|
||||||
|
)
|
||||||
const targetDay = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
|
const targetDay = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
|
||||||
const diffDays = Math.round((targetDay - currentDay) / (1000 * 60 * 60 * 24))
|
const diffDays = Math.round((targetDay - currentDay) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,7 @@ describe("schema sync", () => {
|
|||||||
|
|
||||||
// JSON Schema structure matches
|
// JSON Schema structure matches
|
||||||
const jsonSchema = enhancementResultJsonSchema
|
const jsonSchema = enhancementResultJsonSchema
|
||||||
expect(Object.keys(jsonSchema.properties).sort()).toEqual(
|
expect(Object.keys(jsonSchema.properties).sort()).toEqual(Object.keys(payload).sort())
|
||||||
Object.keys(payload).sort(),
|
|
||||||
)
|
|
||||||
expect([...jsonSchema.required].sort()).toEqual(Object.keys(payload).sort())
|
expect([...jsonSchema.required].sort()).toEqual(Object.keys(payload).sort())
|
||||||
|
|
||||||
// syntheticItems item schema has the right required fields
|
// syntheticItems item schema has the right required fields
|
||||||
@@ -167,11 +165,7 @@ describe("schema sync", () => {
|
|||||||
|
|
||||||
// JSON Schema only allows string or null for slot values
|
// JSON Schema only allows string or null for slot values
|
||||||
const slotValueSchema =
|
const slotValueSchema =
|
||||||
enhancementResultJsonSchema.properties.slotFills.additionalProperties
|
enhancementResultJsonSchema.properties.slotFills.additionalProperties.additionalProperties
|
||||||
.additionalProperties
|
expect(slotValueSchema.anyOf).toEqual([{ type: "string" }, { type: "null" }])
|
||||||
expect(slotValueSchema.anyOf).toEqual([
|
|
||||||
{ type: "string" },
|
|
||||||
{ type: "null" },
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomBytes } from "node:crypto"
|
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { randomBytes } from "node:crypto"
|
||||||
|
|
||||||
import { CredentialEncryptor } from "./crypto.ts"
|
import { CredentialEncryptor } from "./crypto.ts"
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
// Delegates to find — row locking is a no-op in tests.
|
||||||
|
if (mockFindResult !== undefined) return mockFindResult
|
||||||
|
const now = new Date()
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
userId,
|
||||||
|
sourceId,
|
||||||
|
enabled: true,
|
||||||
|
config: {},
|
||||||
|
credentials: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateConfig(_sourceId: string, _update: { enabled?: boolean; config?: unknown }) {
|
||||||
|
// no-op for tests
|
||||||
|
},
|
||||||
async upsertConfig(_sourceId: string, _data: { enabled: boolean; config: unknown }) {
|
async upsertConfig(_sourceId: string, _data: { enabled: boolean; config: unknown }) {
|
||||||
// no-op for tests
|
// no-op for tests
|
||||||
},
|
},
|
||||||
@@ -93,7 +111,9 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fakeDb = {} as Database
|
const fakeDb = {
|
||||||
|
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
|
||||||
|
} as unknown as Database
|
||||||
|
|
||||||
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
|
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -126,11 +126,10 @@ export class UserSessionManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the existing row for config merging and credential access.
|
// Use a transaction with SELECT FOR UPDATE to prevent lost updates
|
||||||
// NOTE: find + updateConfig is not atomic. A concurrent update could
|
// when concurrent PATCH requests merge config against the same base.
|
||||||
// read stale config. Use SELECT FOR UPDATE or atomic jsonb merge if
|
const { existingRow, mergedConfig } = await this.db.transaction(async (tx) => {
|
||||||
// this becomes a problem.
|
const existingRow = await sources(tx, userId).findForUpdate(sourceId)
|
||||||
const existingRow = await sources(this.db, userId).find(sourceId)
|
|
||||||
|
|
||||||
let mergedConfig: Record<string, unknown> | undefined
|
let mergedConfig: Record<string, unknown> | undefined
|
||||||
if (update.config !== undefined && provider.configSchema) {
|
if (update.config !== undefined && provider.configSchema) {
|
||||||
@@ -144,11 +143,14 @@ export class UserSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Throws SourceNotFoundError if the row doesn't exist
|
// Throws SourceNotFoundError if the row doesn't exist
|
||||||
await sources(this.db, userId).updateConfig(sourceId, {
|
await sources(tx, userId).updateConfig(sourceId, {
|
||||||
enabled: update.enabled,
|
enabled: update.enabled,
|
||||||
config: mergedConfig,
|
config: mergedConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return { existingRow, mergedConfig }
|
||||||
|
})
|
||||||
|
|
||||||
// Refresh the specific source in the active session instead of
|
// Refresh the specific source in the active session instead of
|
||||||
// destroying the entire session.
|
// destroying the entire session.
|
||||||
const session = this.sessions.get(userId)
|
const session = this.sessions.get(userId)
|
||||||
@@ -202,21 +204,24 @@ export class UserSessionManager {
|
|||||||
|
|
||||||
const config = data.config ?? {}
|
const config = data.config ?? {}
|
||||||
|
|
||||||
// Fetch existing row before upsert to capture credentials for session refresh.
|
// Run the upsert + credential update atomically so a failure in
|
||||||
// For new rows this will be undefined — credentials will be null.
|
// either step doesn't leave the row in an inconsistent state.
|
||||||
const existingRow = await sources(this.db, userId).find(sourceId)
|
const existingRow = await this.db.transaction(async (tx) => {
|
||||||
|
const existing = await sources(tx, userId).find(sourceId)
|
||||||
|
|
||||||
await sources(this.db, userId).upsertConfig(sourceId, {
|
await sources(tx, userId).upsertConfig(sourceId, {
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Persist credentials after the upsert so the row exists.
|
|
||||||
if (data.credentials !== undefined && this.encryptor) {
|
if (data.credentials !== undefined && this.encryptor) {
|
||||||
const encrypted = this.encryptor.encrypt(JSON.stringify(data.credentials))
|
const encrypted = this.encryptor.encrypt(JSON.stringify(data.credentials))
|
||||||
await sources(this.db, userId).updateCredentials(sourceId, encrypted)
|
await sources(tx, userId).updateCredentials(sourceId, encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
|
})
|
||||||
|
|
||||||
const session = this.sessions.get(userId)
|
const session = this.sessions.get(userId)
|
||||||
if (session) {
|
if (session) {
|
||||||
if (!data.enabled) {
|
if (!data.enabled) {
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ function createInMemoryStore() {
|
|||||||
async find(sourceId: string) {
|
async find(sourceId: string) {
|
||||||
return rows.get(key(userId, sourceId))
|
return rows.get(key(userId, sourceId))
|
||||||
},
|
},
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
return rows.get(key(userId, sourceId))
|
||||||
|
},
|
||||||
async updateConfig(sourceId: string, update: { enabled?: boolean; config?: unknown }) {
|
async updateConfig(sourceId: string, update: { enabled?: boolean; config?: unknown }) {
|
||||||
const existing = rows.get(key(userId, sourceId))
|
const existing = rows.get(key(userId, sourceId))
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -125,7 +128,9 @@ mock.module("../sources/user-sources.ts", () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const fakeDb = {} as Database
|
const fakeDb = {
|
||||||
|
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
|
||||||
|
} as unknown as Database
|
||||||
|
|
||||||
function createApp(providers: FeedSourceProvider[], userId?: string) {
|
function createApp(providers: FeedSourceProvider[], userId?: string) {
|
||||||
const sessionManager = new UserSessionManager({ providers, db: fakeDb })
|
const sessionManager = new UserSessionManager({ providers, db: fakeDb })
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export function sources(db: Database, userId: string) {
|
|||||||
return rows[0]
|
return rows[0]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Like find(), but acquires a row lock to prevent concurrent modifications. Must be called inside a transaction. */
|
||||||
|
async findForUpdate(sourceId: string) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(userSources)
|
||||||
|
.where(and(eq(userSources.userId, userId), eq(userSources.sourceId, sourceId)))
|
||||||
|
.limit(1)
|
||||||
|
.for("update")
|
||||||
|
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
|
||||||
/** Enables a source for the user. Throws if the source row doesn't exist. */
|
/** Enables a source for the user. Throws if the source row doesn't exist. */
|
||||||
async enableSource(sourceId: string) {
|
async enableSource(sourceId: string) {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
|
|||||||
@@ -55,44 +55,112 @@
|
|||||||
"fontFamily": "Inter",
|
"fontFamily": "Inter",
|
||||||
"fontDefinitions": [
|
"fontDefinitions": [
|
||||||
{ "path": "./assets/fonts/Inter_100Thin.ttf", "weight": 100 },
|
{ "path": "./assets/fonts/Inter_100Thin.ttf", "weight": 100 },
|
||||||
{ "path": "./assets/fonts/Inter_100Thin_Italic.ttf", "weight": 100, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_100Thin_Italic.ttf",
|
||||||
|
"weight": 100,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_200ExtraLight.ttf", "weight": 200 },
|
{ "path": "./assets/fonts/Inter_200ExtraLight.ttf", "weight": 200 },
|
||||||
{ "path": "./assets/fonts/Inter_200ExtraLight_Italic.ttf", "weight": 200, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_200ExtraLight_Italic.ttf",
|
||||||
|
"weight": 200,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_300Light.ttf", "weight": 300 },
|
{ "path": "./assets/fonts/Inter_300Light.ttf", "weight": 300 },
|
||||||
{ "path": "./assets/fonts/Inter_300Light_Italic.ttf", "weight": 300, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_300Light_Italic.ttf",
|
||||||
|
"weight": 300,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_400Regular.ttf", "weight": 400 },
|
{ "path": "./assets/fonts/Inter_400Regular.ttf", "weight": 400 },
|
||||||
{ "path": "./assets/fonts/Inter_400Regular_Italic.ttf", "weight": 400, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_400Regular_Italic.ttf",
|
||||||
|
"weight": 400,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_500Medium.ttf", "weight": 500 },
|
{ "path": "./assets/fonts/Inter_500Medium.ttf", "weight": 500 },
|
||||||
{ "path": "./assets/fonts/Inter_500Medium_Italic.ttf", "weight": 500, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_500Medium_Italic.ttf",
|
||||||
|
"weight": 500,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_600SemiBold.ttf", "weight": 600 },
|
{ "path": "./assets/fonts/Inter_600SemiBold.ttf", "weight": 600 },
|
||||||
{ "path": "./assets/fonts/Inter_600SemiBold_Italic.ttf", "weight": 600, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_600SemiBold_Italic.ttf",
|
||||||
|
"weight": 600,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_700Bold.ttf", "weight": 700 },
|
{ "path": "./assets/fonts/Inter_700Bold.ttf", "weight": 700 },
|
||||||
{ "path": "./assets/fonts/Inter_700Bold_Italic.ttf", "weight": 700, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_700Bold_Italic.ttf",
|
||||||
|
"weight": 700,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_800ExtraBold.ttf", "weight": 800 },
|
{ "path": "./assets/fonts/Inter_800ExtraBold.ttf", "weight": 800 },
|
||||||
{ "path": "./assets/fonts/Inter_800ExtraBold_Italic.ttf", "weight": 800, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/Inter_800ExtraBold_Italic.ttf",
|
||||||
|
"weight": 800,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/Inter_900Black.ttf", "weight": 900 },
|
{ "path": "./assets/fonts/Inter_900Black.ttf", "weight": 900 },
|
||||||
{ "path": "./assets/fonts/Inter_900Black_Italic.ttf", "weight": 900, "style": "italic" }
|
{
|
||||||
|
"path": "./assets/fonts/Inter_900Black_Italic.ttf",
|
||||||
|
"weight": 900,
|
||||||
|
"style": "italic"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fontFamily": "Source Serif 4",
|
"fontFamily": "Source Serif 4",
|
||||||
"fontDefinitions": [
|
"fontDefinitions": [
|
||||||
{ "path": "./assets/fonts/SourceSerif4_200ExtraLight.ttf", "weight": 200 },
|
{ "path": "./assets/fonts/SourceSerif4_200ExtraLight.ttf", "weight": 200 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_200ExtraLight_Italic.ttf", "weight": 200, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_200ExtraLight_Italic.ttf",
|
||||||
|
"weight": 200,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_300Light.ttf", "weight": 300 },
|
{ "path": "./assets/fonts/SourceSerif4_300Light.ttf", "weight": 300 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_300Light_Italic.ttf", "weight": 300, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_300Light_Italic.ttf",
|
||||||
|
"weight": 300,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_400Regular.ttf", "weight": 400 },
|
{ "path": "./assets/fonts/SourceSerif4_400Regular.ttf", "weight": 400 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_400Regular_Italic.ttf", "weight": 400, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_400Regular_Italic.ttf",
|
||||||
|
"weight": 400,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_500Medium.ttf", "weight": 500 },
|
{ "path": "./assets/fonts/SourceSerif4_500Medium.ttf", "weight": 500 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_500Medium_Italic.ttf", "weight": 500, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_500Medium_Italic.ttf",
|
||||||
|
"weight": 500,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_600SemiBold.ttf", "weight": 600 },
|
{ "path": "./assets/fonts/SourceSerif4_600SemiBold.ttf", "weight": 600 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_600SemiBold_Italic.ttf", "weight": 600, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_600SemiBold_Italic.ttf",
|
||||||
|
"weight": 600,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_700Bold.ttf", "weight": 700 },
|
{ "path": "./assets/fonts/SourceSerif4_700Bold.ttf", "weight": 700 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_700Bold_Italic.ttf", "weight": 700, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_700Bold_Italic.ttf",
|
||||||
|
"weight": 700,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_800ExtraBold.ttf", "weight": 800 },
|
{ "path": "./assets/fonts/SourceSerif4_800ExtraBold.ttf", "weight": 800 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_800ExtraBold_Italic.ttf", "weight": 800, "style": "italic" },
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_800ExtraBold_Italic.ttf",
|
||||||
|
"weight": 800,
|
||||||
|
"style": "italic"
|
||||||
|
},
|
||||||
{ "path": "./assets/fonts/SourceSerif4_900Black.ttf", "weight": 900 },
|
{ "path": "./assets/fonts/SourceSerif4_900Black.ttf", "weight": 900 },
|
||||||
{ "path": "./assets/fonts/SourceSerif4_900Black_Italic.ttf", "weight": 900, "style": "italic" }
|
{
|
||||||
|
"path": "./assets/fonts/SourceSerif4_900Black_Italic.ttf",
|
||||||
|
"weight": 900,
|
||||||
|
"style": "italic"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export class ApiClient {
|
|||||||
(prevInit, middleware) => middleware(url, prevInit),
|
(prevInit, middleware) => middleware(url, prevInit),
|
||||||
init,
|
init,
|
||||||
)
|
)
|
||||||
return fetch(this.baseUrl ? new URL(url.toString(), this.baseUrl) : url, finalInit).then((res) =>
|
return fetch(this.baseUrl ? new URL(url.toString(), this.baseUrl) : url, finalInit).then(
|
||||||
Promise.all([Promise.resolve(res), res.json()]),
|
(res) => Promise.all([Promise.resolve(res), res.json()]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { useEffect } from "react"
|
|||||||
import { ScrollView, View } from "react-native"
|
import { ScrollView, View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
|
import { type Showcase } from "@/components/showcase"
|
||||||
import { buttonShowcase } from "@/components/ui/button.showcase"
|
import { buttonShowcase } from "@/components/ui/button.showcase"
|
||||||
import { feedCardShowcase } from "@/components/ui/feed-card.showcase"
|
import { feedCardShowcase } from "@/components/ui/feed-card.showcase"
|
||||||
import { monospaceTextShowcase } from "@/components/ui/monospace-text.showcase"
|
import { monospaceTextShowcase } from "@/components/ui/monospace-text.showcase"
|
||||||
|
import { SansSerifText } from "@/components/ui/sans-serif-text"
|
||||||
import { sansSerifTextShowcase } from "@/components/ui/sans-serif-text.showcase"
|
import { sansSerifTextShowcase } from "@/components/ui/sans-serif-text.showcase"
|
||||||
import { serifTextShowcase } from "@/components/ui/serif-text.showcase"
|
import { serifTextShowcase } from "@/components/ui/serif-text.showcase"
|
||||||
import { type Showcase } from "@/components/showcase"
|
|
||||||
import { SansSerifText } from "@/components/ui/sans-serif-text"
|
|
||||||
|
|
||||||
const showcases: Record<string, Showcase> = {
|
const showcases: Record<string, Showcase> = {
|
||||||
button: buttonShowcase,
|
button: buttonShowcase,
|
||||||
@@ -41,7 +41,10 @@ export default function ComponentDetailScreen() {
|
|||||||
const ShowcaseComponent = showcase.component
|
const ShowcaseComponent = showcase.component
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={tw`bg-stone-100 dark:bg-stone-900 flex-1`} contentContainerStyle={tw`px-5 pb-10 pt-4 gap-6`}>
|
<ScrollView
|
||||||
|
style={tw`bg-stone-100 dark:bg-stone-900 flex-1`}
|
||||||
|
contentContainerStyle={tw`px-5 pb-10 pt-4 gap-6`}
|
||||||
|
>
|
||||||
<ShowcaseComponent />
|
<ShowcaseComponent />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ const components = [
|
|||||||
export default function ComponentsScreen() {
|
export default function ComponentsScreen() {
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1`}>
|
<View style={tw`flex-1`}>
|
||||||
<View style={tw`mx-4 mt-4 rounded-xl border border-stone-200 dark:border-stone-800 overflow-hidden`}>
|
<View
|
||||||
|
style={tw`mx-4 mt-4 rounded-xl border border-stone-200 dark:border-stone-800 overflow-hidden`}
|
||||||
|
>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={components}
|
data={components}
|
||||||
keyExtractor={(item) => item.name}
|
keyExtractor={(item) => item.name}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { View } from "react-native"
|
import { View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
import { Button } from "./button"
|
|
||||||
import { type Showcase, Section } from "../showcase"
|
import { type Showcase, Section } from "../showcase"
|
||||||
|
import { Button } from "./button"
|
||||||
|
|
||||||
function ButtonShowcase() {
|
function ButtonShowcase() {
|
||||||
return (
|
return (
|
||||||
@@ -11,11 +11,7 @@ function ButtonShowcase() {
|
|||||||
<Button style={tw`self-start`} label="Press me" />
|
<Button style={tw`self-start`} label="Press me" />
|
||||||
</Section>
|
</Section>
|
||||||
<Section title="Leading icon">
|
<Section title="Leading icon">
|
||||||
<Button
|
<Button style={tw`self-start`} label="Add item" leadingIcon={<Button.Icon name="plus" />} />
|
||||||
style={tw`self-start`}
|
|
||||||
label="Add item"
|
|
||||||
leadingIcon={<Button.Icon name="plus" />}
|
|
||||||
/>
|
|
||||||
</Section>
|
</Section>
|
||||||
<Section title="Trailing icon">
|
<Section title="Trailing icon">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ type ButtonProps = Omit<PressableProps, "children"> & {
|
|||||||
export function Button({ style, label, leadingIcon, trailingIcon, ...props }: ButtonProps) {
|
export function Button({ style, label, leadingIcon, trailingIcon, ...props }: ButtonProps) {
|
||||||
const hasIcons = leadingIcon != null || trailingIcon != null
|
const hasIcons = leadingIcon != null || trailingIcon != null
|
||||||
|
|
||||||
const textElement = <SansSerifText style={tw`text-stone-100 dark:text-stone-200 font-medium`}>{label}</SansSerifText>
|
const textElement = (
|
||||||
|
<SansSerifText style={tw`text-stone-100 dark:text-stone-200 font-medium`}>
|
||||||
|
{label}
|
||||||
|
</SansSerifText>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable style={[tw`rounded-full bg-teal-600 px-4 py-3 w-fit`, style]} {...props}>
|
<Pressable style={[tw`rounded-full bg-teal-600 px-4 py-3 w-fit`, style]} {...props}>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { View } from "react-native"
|
import { View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
|
import { type Showcase, Section } from "../showcase"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
import { FeedCard } from "./feed-card"
|
import { FeedCard } from "./feed-card"
|
||||||
import { SansSerifText } from "./sans-serif-text"
|
import { SansSerifText } from "./sans-serif-text"
|
||||||
import { SerifText } from "./serif-text"
|
import { SerifText } from "./serif-text"
|
||||||
import { type Showcase, Section } from "../showcase"
|
|
||||||
|
|
||||||
function FeedCardShowcase() {
|
function FeedCardShowcase() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,5 +2,10 @@ import { View, type ViewProps } from "react-native"
|
|||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
export function FeedCard({ style, ...props }: ViewProps) {
|
export function FeedCard({ style, ...props }: ViewProps) {
|
||||||
return <View style={[tw`border border-stone-200 dark:border-stone-800 rounded-lg`, style]} {...props} />
|
return (
|
||||||
|
<View
|
||||||
|
style={[tw`border border-stone-200 dark:border-stone-800 rounded-lg`, style]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { View } from "react-native"
|
import { View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
import { MonospaceText } from "./monospace-text"
|
|
||||||
import { type Showcase, Section } from "../showcase"
|
import { type Showcase, Section } from "../showcase"
|
||||||
|
import { MonospaceText } from "./monospace-text"
|
||||||
|
|
||||||
function MonospaceTextShowcase() {
|
function MonospaceTextShowcase() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import tw from "twrnc"
|
|||||||
|
|
||||||
export function MonospaceText({ children, style, ...props }: TextProps) {
|
export function MonospaceText({ children, style, ...props }: TextProps) {
|
||||||
return (
|
return (
|
||||||
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Menlo" }, style]} {...props}>
|
<Text
|
||||||
|
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Menlo" }, style]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { View } from "react-native"
|
import { View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
import { SansSerifText } from "./sans-serif-text"
|
|
||||||
import { type Showcase, Section } from "../showcase"
|
import { type Showcase, Section } from "../showcase"
|
||||||
|
import { SansSerifText } from "./sans-serif-text"
|
||||||
|
|
||||||
function SansSerifTextShowcase() {
|
function SansSerifTextShowcase() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import tw from "twrnc"
|
|||||||
|
|
||||||
export function SansSerifText({ children, style, ...props }: TextProps) {
|
export function SansSerifText({ children, style, ...props }: TextProps) {
|
||||||
return (
|
return (
|
||||||
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Inter" }, style]} {...props}>
|
<Text
|
||||||
|
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Inter" }, style]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { View } from "react-native"
|
import { View } from "react-native"
|
||||||
import tw from "twrnc"
|
import tw from "twrnc"
|
||||||
|
|
||||||
import { SerifText } from "./serif-text"
|
|
||||||
import { type Showcase, Section } from "../showcase"
|
import { type Showcase, Section } from "../showcase"
|
||||||
|
import { SerifText } from "./serif-text"
|
||||||
|
|
||||||
function SerifTextShowcase() {
|
function SerifTextShowcase() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import tw from "twrnc"
|
|||||||
|
|
||||||
export function SerifText({ children, style, ...props }: TextProps) {
|
export function SerifText({ children, style, ...props }: TextProps) {
|
||||||
return (
|
return (
|
||||||
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Source Serif 4" }, style]} {...props}>
|
<Text
|
||||||
|
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Source Serif 4" }, style]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export const catalog = defineCatalog(schema, {
|
|||||||
style: z.string().nullable(),
|
style: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
slots: ["default"],
|
slots: ["default"],
|
||||||
description: "Bordered card container for feed content. The style prop accepts a twrnc class string.",
|
description:
|
||||||
|
"Bordered card container for feed content. The style prop accepts a twrnc class string.",
|
||||||
example: { style: "p-4 gap-2" },
|
example: { style: "p-4 gap-2" },
|
||||||
},
|
},
|
||||||
SansSerifText: {
|
SansSerifText: {
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ type ButtonIconName = React.ComponentProps<typeof Button.Icon>["name"]
|
|||||||
|
|
||||||
export const { registry } = defineRegistry(catalog, {
|
export const { registry } = defineRegistry(catalog, {
|
||||||
components: {
|
components: {
|
||||||
View: ({ props, children }) => <View style={props.style ? tw`${props.style}` : undefined}>{children}</View>,
|
View: ({ props, children }) => (
|
||||||
|
<View style={props.style ? tw`${props.style}` : undefined}>{children}</View>
|
||||||
|
),
|
||||||
Button: ({ props, emit }) => (
|
Button: ({ props, emit }) => (
|
||||||
<Button
|
<Button
|
||||||
label={props.label}
|
label={props.label}
|
||||||
leadingIcon={props.leadingIcon ? <Button.Icon name={props.leadingIcon as ButtonIconName} /> : undefined}
|
leadingIcon={
|
||||||
trailingIcon={props.trailingIcon ? <Button.Icon name={props.trailingIcon as ButtonIconName} /> : undefined}
|
props.leadingIcon ? <Button.Icon name={props.leadingIcon as ButtonIconName} /> : undefined
|
||||||
|
}
|
||||||
|
trailingIcon={
|
||||||
|
props.trailingIcon ? (
|
||||||
|
<Button.Icon name={props.trailingIcon as ButtonIconName} />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
onPress={() => emit("press")}
|
onPress={() => emit("press")}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -27,13 +35,17 @@ export const { registry } = defineRegistry(catalog, {
|
|||||||
<FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard>
|
<FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard>
|
||||||
),
|
),
|
||||||
SansSerifText: ({ props }) => (
|
SansSerifText: ({ props }) => (
|
||||||
<SansSerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SansSerifText>
|
<SansSerifText style={props.style ? tw`${props.style}` : undefined}>
|
||||||
|
{props.text}
|
||||||
|
</SansSerifText>
|
||||||
),
|
),
|
||||||
SerifText: ({ props }) => (
|
SerifText: ({ props }) => (
|
||||||
<SerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SerifText>
|
<SerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SerifText>
|
||||||
),
|
),
|
||||||
MonospaceText: ({ props }) => (
|
MonospaceText: ({ props }) => (
|
||||||
<MonospaceText style={props.style ? tw`${props.style}` : undefined}>{props.text}</MonospaceText>
|
<MonospaceText style={props.style ? tw`${props.style}` : undefined}>
|
||||||
|
{props.text}
|
||||||
|
</MonospaceText>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1,131 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[-10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":7,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":7,"st":0}],"meta":{"g":"https://jitter.video"},"op":6,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 53] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 122] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": { "a": 0, "k": [160, 53] },
|
||||||
|
"p": { "a": 0, "k": [200.5, 200] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 6, "s": [-10], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 53] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 122] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": { "a": 0, "k": [160, 53] },
|
||||||
|
"p": { "a": 0, "k": [200.594, 200.176] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 6, "s": [10], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 7, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 7, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 6,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,131 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[-10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":7,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":7,"st":0}],"meta":{"g":"https://jitter.video"},"op":6,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 53] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 122] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": { "a": 0, "k": [160, 53] },
|
||||||
|
"p": { "a": 0, "k": [200.5, 200] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 6, "s": [-10], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 53] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 122] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": { "a": 0, "k": [160, 53] },
|
||||||
|
"p": { "a": 0, "k": [200.594, 200.176] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 6, "s": [10], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 7,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 7, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 7, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 6,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,281 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200,200.014]},"r":{"a":1,"k":[{"t":0,"s":[-90],"h":1},{"t":8.4,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"h":1},{"t":37.8,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.094,200.19]},"r":{"a":1,"k":[{"t":0,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":61,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":61,"st":0}],"meta":{"g":"https://jitter.video"},"op":60,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200, 200.014] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-90], "h": 1 },
|
||||||
|
{ "t": 8.4, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [0], "h": 1 },
|
||||||
|
{ "t": 37.8, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 60, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.094, 200.19] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 60, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 61, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 61, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 60,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,281 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200,200.014]},"r":{"a":1,"k":[{"t":0,"s":[-90],"h":1},{"t":8.4,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"h":1},{"t":37.8,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.094,200.19]},"r":{"a":1,"k":[{"t":0,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":61,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":61,"st":0}],"meta":{"g":"https://jitter.video"},"op":60,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8.4,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 37.8,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200, 200.014] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-90], "h": 1 },
|
||||||
|
{ "t": 8.4, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [0], "h": 1 },
|
||||||
|
{ "t": 37.8, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 60, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 1],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 0.5],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 30,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0.5], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.094, 200.19] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
|
||||||
|
{ "t": 60, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 61,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 61, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 61, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 60,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,224 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[320,106],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"h":1},{"t":5.28,"s":[-30],"i":{"x":0.001,"y":0.998},"o":{"x":0.349,"y":0}},{"t":30,"s":[-90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":0.999},"o":{"x":0.348,"y":0}},{"t":30,"s":[90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":31,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":31,"st":0}],"meta":{"g":"https://jitter.video"},"op":30,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.5, 200] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-30], "h": 1 },
|
||||||
|
{ "t": 5.28, "s": [-30], "i": { "x": 0.001, "y": 0.998 }, "o": { "x": 0.349, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [-90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.594, 200.176] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [30], "i": { "x": 0, "y": 0.999 }, "o": { "x": 0.348, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 31, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 31, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 30,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,224 @@
|
|||||||
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[320,106],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"h":1},{"t":5.28,"s":[-30],"i":{"x":0.001,"y":0.998},"o":{"x":0.349,"y":0}},{"t":30,"s":[-90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":0.999},"o":{"x":0.348,"y":0}},{"t":30,"s":[90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":31,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":31,"st":0}],"meta":{"g":"https://jitter.video"},"op":30,"v":"5.7.4","w":400}
|
{
|
||||||
|
"fr": 60,
|
||||||
|
"h": 400,
|
||||||
|
"ip": 0,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 2,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 1], "y": [1, 1] },
|
||||||
|
"o": { "x": [0, 0], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 5.28,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0.001], "y": [1, 0.998] },
|
||||||
|
"o": { "x": [0, 0.349], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.5, 200] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [-30], "h": 1 },
|
||||||
|
{ "t": 5.28, "s": [-30], "i": { "x": 0.001, "y": 0.998 }, "o": { "x": 0.349, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [-90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"parent": 4,
|
||||||
|
"ks": {},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "rc",
|
||||||
|
"p": { "a": 0, "k": [160, 26.75] },
|
||||||
|
"r": { "a": 0, "k": 0 },
|
||||||
|
"s": { "a": 0, "k": [336, 174.5] }
|
||||||
|
},
|
||||||
|
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [320, 106],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [320, 1], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
|
||||||
|
"lc": 1,
|
||||||
|
"lj": 1,
|
||||||
|
"ml": 10,
|
||||||
|
"o": { "a": 0, "k": 100 },
|
||||||
|
"w": { "a": 0, "k": 16 }
|
||||||
|
},
|
||||||
|
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 3,
|
||||||
|
"parent": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"t": 0,
|
||||||
|
"s": [160, 53],
|
||||||
|
"i": { "x": [1, 0], "y": [1, 0.999] },
|
||||||
|
"o": { "x": [0, 0.348], "y": [0, 0] }
|
||||||
|
},
|
||||||
|
{ "t": 30, "s": [160, 0.5], "h": 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [200.594, 200.176] },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{ "t": 0, "s": [30], "i": { "x": 0, "y": 0.999 }, "o": { "x": 0.348, "y": 0 } },
|
||||||
|
{ "t": 30, "s": [90], "h": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 31,
|
||||||
|
"st": 0
|
||||||
|
},
|
||||||
|
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 31, "st": 0 },
|
||||||
|
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 31, "st": 0 }
|
||||||
|
],
|
||||||
|
"meta": { "g": "https://jitter.video" },
|
||||||
|
"op": 30,
|
||||||
|
"v": "5.7.4",
|
||||||
|
"w": 400
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ primary_region = 'lhr'
|
|||||||
[build]
|
[build]
|
||||||
|
|
||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 3000
|
internal_port = 3000
|
||||||
force_https = true
|
force_https = true
|
||||||
auto_stop_machines = 'stop'
|
auto_stop_machines = 'stop'
|
||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
min_machines_running = 0
|
min_machines_running = 0
|
||||||
processes = ['app']
|
processes = ['app']
|
||||||
|
|
||||||
[[vm]]
|
[[vm]]
|
||||||
memory = '1gb'
|
memory = '1gb'
|
||||||
cpus = 1
|
cpus = 1
|
||||||
memory_mb = 1024
|
memory_mb = 1024
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Sources → Source Graph → FeedEngine
|
|||||||
|
|
||||||
### One harness, not many agents
|
### One harness, not many agents
|
||||||
|
|
||||||
The "agents" in this doc describe *behaviors*, not separate running processes. A human PA is one person — they don't have a "calendar agent" and a "follow-up agent" in their head. They look at your whole situation and act on whatever matters.
|
The "agents" in this doc describe _behaviors_, not separate running processes. A human PA is one person — they don't have a "calendar agent" and a "follow-up agent" in their head. They look at your whole situation and act on whatever matters.
|
||||||
|
|
||||||
AELIS works the same way. One LLM harness receives all feed items, all context, all user memory, and all available tools. It returns a single `FeedEnhancement`. Every behavior (preparation, follow-up, anomaly detection, tone adjustment, cross-source reasoning) is an instruction in the system prompt, not a separate agent.
|
AELIS works the same way. One LLM harness receives all feed items, all context, all user memory, and all available tools. It returns a single `FeedEnhancement`. Every behavior (preparation, follow-up, anomaly detection, tone adjustment, cross-source reasoning) is an instruction in the system prompt, not a separate agent.
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ The advantage: the LLM sees everything at once. It doesn't need agent-to-agent c
|
|||||||
The only separate LLM call is the **Query Agent** — because it's user-initiated and synchronous. But it uses the same system prompt and context. It's the same "person," just responding to a question instead of proactively enhancing the feed.
|
The only separate LLM call is the **Query Agent** — because it's user-initiated and synchronous. But it uses the same system prompt and context. It's the same "person," just responding to a question instead of proactively enhancing the feed.
|
||||||
|
|
||||||
Everything else is either:
|
Everything else is either:
|
||||||
|
|
||||||
- **Rule-based post-processors** — pure functions, no LLM, run on every refresh
|
- **Rule-based post-processors** — pure functions, no LLM, run on every refresh
|
||||||
- **The single LLM harness** — runs periodically, produces cached `FeedEnhancement`
|
- **The single LLM harness** — runs periodically, produces cached `FeedEnhancement`
|
||||||
- **Background jobs** — daily summary compression, weekly pattern discovery
|
- **Background jobs** — daily summary compression, weekly pattern discovery
|
||||||
@@ -57,7 +58,7 @@ Everything else is either:
|
|||||||
### Component categories
|
### Component categories
|
||||||
|
|
||||||
| Component | What it is | Examples |
|
| Component | What it is | Examples |
|
||||||
|---|---|---|
|
| ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------- |
|
||||||
| **FeedSource nodes** | Graph participants that produce items | Briefing, Preparation, Anomaly Detection, Follow-up, Social Awareness |
|
| **FeedSource nodes** | Graph participants that produce items | Briefing, Preparation, Anomaly Detection, Follow-up, Social Awareness |
|
||||||
| **Rule-based post-processors** | Pure functions that rerank/filter/group | TimeOfDay, CalendarGrouping, Deduplication, UserAffinity |
|
| **Rule-based post-processors** | Pure functions that rerank/filter/group | TimeOfDay, CalendarGrouping, Deduplication, UserAffinity |
|
||||||
| **LLM enhancement harness** | Single background LLM call, cached output | Card rewriting, cross-source synthesis, tone, narrative arcs |
|
| **LLM enhancement harness** | Single background LLM call, cached output | Card rewriting, cross-source synthesis, tone, narrative arcs |
|
||||||
@@ -71,7 +72,7 @@ The LLM harness and post-processors need a unified view of the user's world: cur
|
|||||||
|
|
||||||
`AgentContext` is **not** on the engine. The engine's job is source orchestration — running sources in dependency order, accumulating context, collecting items. It shouldn't know about user preferences, conversation history, or feed snapshots. Those are separate concerns.
|
`AgentContext` is **not** on the engine. The engine's job is source orchestration — running sources in dependency order, accumulating context, collecting items. It shouldn't know about user preferences, conversation history, or feed snapshots. Those are separate concerns.
|
||||||
|
|
||||||
`AgentContext` is a separate object that *reads from* the engine and composes its output with other data stores:
|
`AgentContext` is a separate object that _reads from_ the engine and composes its output with other data stores:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface AgentContext {
|
interface AgentContext {
|
||||||
@@ -142,7 +143,7 @@ interface FeedEnhancement {
|
|||||||
annotations: Record<string, string>
|
annotations: Record<string, string>
|
||||||
|
|
||||||
/** Items to group together with a summary card */
|
/** Items to group together with a summary card */
|
||||||
groups: Array<{ itemIds: string[], summary: string }>
|
groups: Array<{ itemIds: string[]; summary: string }>
|
||||||
|
|
||||||
/** Item IDs to suppress or deprioritize */
|
/** Item IDs to suppress or deprioritize */
|
||||||
suppress: string[]
|
suppress: string[]
|
||||||
@@ -185,6 +186,7 @@ These run on every refresh. Fast, deterministic, and cover most of the ranking q
|
|||||||
**Anomaly detection.** Compare event start times against the user's historical distribution. A 6am meeting when the user never has meetings before 9am is a statistical outlier — flag it.
|
**Anomaly detection.** Compare event start times against the user's historical distribution. A 6am meeting when the user never has meetings before 9am is a statistical outlier — flag it.
|
||||||
|
|
||||||
**User affinity scoring.** Track implicit signals per source type per time-of-day bucket:
|
**User affinity scoring.** Track implicit signals per source type per time-of-day bucket:
|
||||||
|
|
||||||
- Dismissals: user swipes away weather cards → decay affinity for weather
|
- Dismissals: user swipes away weather cards → decay affinity for weather
|
||||||
- Taps: user taps calendar items frequently → boost affinity for calendar
|
- Taps: user taps calendar items frequently → boost affinity for calendar
|
||||||
- Dwell time: user reads TfL alerts carefully → boost
|
- Dwell time: user reads TfL alerts carefully → boost
|
||||||
@@ -309,7 +311,7 @@ There are three layers:
|
|||||||
|
|
||||||
None of these have `if` statements. The LLM reads the feed, reads the user's memory, and decides what to say. Add a new source (Spotify, email, tasks) and the LLM automatically incorporates it — no new behavior code needed.
|
None of these have `if` statements. The LLM reads the feed, reads the user's memory, and decides what to say. Add a new source (Spotify, email, tasks) and the LLM automatically incorporates it — no new behavior code needed.
|
||||||
|
|
||||||
**Infrastructure (plumbing needed, but logic is emergent).** These need tables, APIs, and background jobs. But the *decision-making* — what to extract, when to surface, how to phrase — is all LLM.
|
**Infrastructure (plumbing needed, but logic is emergent).** These need tables, APIs, and background jobs. But the _decision-making_ — what to extract, when to surface, how to phrase — is all LLM.
|
||||||
|
|
||||||
- Gentle Follow-up — needs: extraction pipeline after each conversation turn, `commitments` table. The LLM decides what counts as a commitment and when to remind.
|
- Gentle Follow-up — needs: extraction pipeline after each conversation turn, `commitments` table. The LLM decides what counts as a commitment and when to remind.
|
||||||
- Memory — needs: `memories` table, read/write API. The LLM decides what to remember and how to use it.
|
- Memory — needs: `memories` table, read/write API. The LLM decides what to remember and how to use it.
|
||||||
@@ -321,7 +323,7 @@ None of these have `if` statements. The LLM reads the feed, reads the user's mem
|
|||||||
- Delegation — needs: confirmation flow, write-back infrastructure. The LLM decides what the user wants done.
|
- Delegation — needs: confirmation flow, write-back infrastructure. The LLM decides what the user wants done.
|
||||||
- Financial Awareness — needs: `financial_events` table, email extraction. The LLM decides what financial events matter.
|
- Financial Awareness — needs: `financial_events` table, email extraction. The LLM decides what financial events matter.
|
||||||
|
|
||||||
**Hardcoded rules (fast path, must be deterministic).** These run on every refresh in <10ms. They *should* be rules because they need to be fast and predictable.
|
**Hardcoded rules (fast path, must be deterministic).** These run on every refresh in <10ms. They _should_ be rules because they need to be fast and predictable.
|
||||||
|
|
||||||
- User affinity scoring — decay/boost math on tap/dismiss events
|
- User affinity scoring — decay/boost math on tap/dismiss events
|
||||||
- Deduplication — title + time matching across sources
|
- Deduplication — title + time matching across sources
|
||||||
@@ -419,10 +421,7 @@ class EnhancementManager {
|
|||||||
private lastInputHash: string | null = null
|
private lastInputHash: string | null = null
|
||||||
private running = false
|
private running = false
|
||||||
|
|
||||||
async enhance(
|
async enhance(items: FeedItem[], context: AgentContext): Promise<FeedEnhancement> {
|
||||||
items: FeedItem[],
|
|
||||||
context: AgentContext,
|
|
||||||
): Promise<FeedEnhancement> {
|
|
||||||
const hash = computeHash(items, context)
|
const hash = computeHash(items, context)
|
||||||
|
|
||||||
// Nothing changed — return cache
|
// Nothing changed — return cache
|
||||||
@@ -438,12 +437,14 @@ class EnhancementManager {
|
|||||||
// Run in background, update cache when done
|
// Run in background, update cache when done
|
||||||
this.running = true
|
this.running = true
|
||||||
this.runHarness(items, context, hash)
|
this.runHarness(items, context, hash)
|
||||||
.then(enhancement => {
|
.then((enhancement) => {
|
||||||
this.cache = enhancement
|
this.cache = enhancement
|
||||||
this.lastInputHash = hash
|
this.lastInputHash = hash
|
||||||
this.notifySubscribers(enhancement)
|
this.notifySubscribers(enhancement)
|
||||||
})
|
})
|
||||||
.finally(() => { this.running = false })
|
.finally(() => {
|
||||||
|
this.running = false
|
||||||
|
})
|
||||||
|
|
||||||
// Return stale cache immediately
|
// Return stale cache immediately
|
||||||
return this.cache ?? emptyEnhancement()
|
return this.cache ?? emptyEnhancement()
|
||||||
@@ -522,7 +523,7 @@ These are `FeedSource` nodes that depend on calendar, tasks, weather, and other
|
|||||||
|
|
||||||
#### Anticipatory Logistics
|
#### Anticipatory Logistics
|
||||||
|
|
||||||
Works backward from events to tell you what you need to *do* to be ready.
|
Works backward from events to tell you what you need to _do_ to be ready.
|
||||||
|
|
||||||
- Flight at 6am → "You need to leave by 4am, which means waking at 3:30. I'd suggest packing tonight."
|
- Flight at 6am → "You need to leave by 4am, which means waking at 3:30. I'd suggest packing tonight."
|
||||||
- Dinner at a new restaurant → "It's a 25-minute walk or 8-minute Uber. Street parking is difficult — there's a car park on the next street."
|
- Dinner at a new restaurant → "It's a 25-minute walk or 8-minute Uber. Street parking is difficult — there's a car park on the next street."
|
||||||
@@ -579,7 +580,7 @@ Tracks loose ends — things you said but never wrote down as tasks.
|
|||||||
- "You told James you'd review his PR — it's been 3 days"
|
- "You told James you'd review his PR — it's been 3 days"
|
||||||
- "You promised to call your mom this weekend"
|
- "You promised to call your mom this weekend"
|
||||||
|
|
||||||
The key difference from task tracking: this catches things that fell through the cracks *because* they were never formalized.
|
The key difference from task tracking: this catches things that fell through the cracks _because_ they were never formalized.
|
||||||
|
|
||||||
**How intent extraction works:**
|
**How intent extraction works:**
|
||||||
|
|
||||||
@@ -614,12 +615,14 @@ Long-term memory of interactions and preferences. Feeds into every other agent.
|
|||||||
A persistent profile that builds over time. Not an agent itself — a system that makes every other agent smarter.
|
A persistent profile that builds over time. Not an agent itself — a system that makes every other agent smarter.
|
||||||
|
|
||||||
Learns from:
|
Learns from:
|
||||||
|
|
||||||
- Explicit statements: "I prefer morning meetings"
|
- Explicit statements: "I prefer morning meetings"
|
||||||
- Implicit behavior: user always dismisses evening suggestions
|
- Implicit behavior: user always dismisses evening suggestions
|
||||||
- Feedback: user rates suggestions as helpful/not
|
- Feedback: user rates suggestions as helpful/not
|
||||||
- Cross-source patterns: always books aisle seats, always picks the cheaper option
|
- Cross-source patterns: always books aisle seats, always picks the cheaper option
|
||||||
|
|
||||||
Used by:
|
Used by:
|
||||||
|
|
||||||
- Proactive Agent suggests restaurants the user would actually like
|
- Proactive Agent suggests restaurants the user would actually like
|
||||||
- Delegation Agent books the right kind of hotel room
|
- Delegation Agent books the right kind of hotel room
|
||||||
- Summary Agent uses the user's preferred level of detail
|
- Summary Agent uses the user's preferred level of detail
|
||||||
@@ -651,17 +654,20 @@ interface DailySummary {
|
|||||||
date: string
|
date: string
|
||||||
feedCheckTimes: string[] // when the user opened the feed
|
feedCheckTimes: string[] // when the user opened the feed
|
||||||
itemTypeCounts: Record<string, number> // how many of each type appeared
|
itemTypeCounts: Record<string, number> // how many of each type appeared
|
||||||
interactions: Array<{ // what the user tapped/dismissed
|
interactions: Array<{
|
||||||
|
// what the user tapped/dismissed
|
||||||
itemType: string
|
itemType: string
|
||||||
action: "tap" | "dismiss" | "dwell"
|
action: "tap" | "dismiss" | "dwell"
|
||||||
time: string
|
time: string
|
||||||
}>
|
}>
|
||||||
locations: Array<{ // where the user was throughout the day
|
locations: Array<{
|
||||||
|
// where the user was throughout the day
|
||||||
lat: number
|
lat: number
|
||||||
lng: number
|
lng: number
|
||||||
time: string
|
time: string
|
||||||
}>
|
}>
|
||||||
calendarSummary: Array<{ // what events happened
|
calendarSummary: Array<{
|
||||||
|
// what events happened
|
||||||
title: string
|
title: string
|
||||||
startTime: string
|
startTime: string
|
||||||
endTime: string
|
endTime: string
|
||||||
@@ -685,7 +691,7 @@ interface DiscoveredPattern {
|
|||||||
/** When this pattern is relevant */
|
/** When this pattern is relevant */
|
||||||
relevance: {
|
relevance: {
|
||||||
daysOfWeek?: number[]
|
daysOfWeek?: number[]
|
||||||
timeRange?: { start: string, end: string }
|
timeRange?: { start: string; end: string }
|
||||||
conditions?: string[]
|
conditions?: string[]
|
||||||
}
|
}
|
||||||
/** How this should affect the feed */
|
/** How this should affect the feed */
|
||||||
@@ -717,9 +723,9 @@ Maintains awareness of relationships and surfaces timely nudges.
|
|||||||
|
|
||||||
Needs: contacts with birthday/anniversary data, calendar history for meeting frequency, email/message signals, optionally social media.
|
Needs: contacts with birthday/anniversary data, calendar history for meeting frequency, email/message signals, optionally social media.
|
||||||
|
|
||||||
This is what makes an assistant feel like it *cares*. Most tools are transactional. This one remembers the people in your life.
|
This is what makes an assistant feel like it _cares_. Most tools are transactional. This one remembers the people in your life.
|
||||||
|
|
||||||
Beyond frequency, the assistant can understand relationship *dynamics*:
|
Beyond frequency, the assistant can understand relationship _dynamics_:
|
||||||
|
|
||||||
- "You and Sarah always have productive meetings. You and Alex tend to go off-track — maybe set a tighter agenda."
|
- "You and Sarah always have productive meetings. You and Alex tend to go off-track — maybe set a tighter agenda."
|
||||||
- "You've cancelled on Tom three times — he might be feeling deprioritized."
|
- "You've cancelled on Tom three times — he might be feeling deprioritized."
|
||||||
@@ -785,7 +791,7 @@ This is where the source graph pays off. All the data is already there — the a
|
|||||||
|
|
||||||
#### Tone & Timing
|
#### Tone & Timing
|
||||||
|
|
||||||
Controls *when* and *how* information is delivered. The difference between useful and annoying.
|
Controls _when_ and _how_ information is delivered. The difference between useful and annoying.
|
||||||
|
|
||||||
- Bad news before morning coffee? Hold it.
|
- Bad news before morning coffee? Hold it.
|
||||||
- Three notifications in a row? Batch them.
|
- Three notifications in a row? Batch them.
|
||||||
@@ -849,6 +855,7 @@ The primary interface. This isn't a feed query tool — it's the person you talk
|
|||||||
The user should be able to ask AELIS anything they'd ask a knowledgeable friend. Some questions are about their data. Most aren't.
|
The user should be able to ask AELIS anything they'd ask a knowledgeable friend. Some questions are about their data. Most aren't.
|
||||||
|
|
||||||
**About their life (reads from the source graph):**
|
**About their life (reads from the source graph):**
|
||||||
|
|
||||||
- "What's on my calendar tomorrow?"
|
- "What's on my calendar tomorrow?"
|
||||||
- "When's my next flight?"
|
- "When's my next flight?"
|
||||||
- "Do I have any conflicts this week?"
|
- "Do I have any conflicts this week?"
|
||||||
@@ -856,6 +863,7 @@ The user should be able to ask AELIS anything they'd ask a knowledgeable friend.
|
|||||||
- "Tell me more about this" (anchored to a feed item)
|
- "Tell me more about this" (anchored to a feed item)
|
||||||
|
|
||||||
**About the world (falls through to web search):**
|
**About the world (falls through to web search):**
|
||||||
|
|
||||||
- "How do I unclog a drain?"
|
- "How do I unclog a drain?"
|
||||||
- "What should I make with chicken and broccoli?"
|
- "What should I make with chicken and broccoli?"
|
||||||
- "What's the best way to get from King's Cross to Heathrow?"
|
- "What's the best way to get from King's Cross to Heathrow?"
|
||||||
@@ -864,6 +872,7 @@ The user should be able to ask AELIS anything they'd ask a knowledgeable friend.
|
|||||||
- "What are some good date night restaurants in Shoreditch?"
|
- "What are some good date night restaurants in Shoreditch?"
|
||||||
|
|
||||||
**Contextual blend (graph + web):**
|
**Contextual blend (graph + web):**
|
||||||
|
|
||||||
- "What's the dress code for The Ivy?" (calendar shows dinner there tonight)
|
- "What's the dress code for The Ivy?" (calendar shows dinner there tonight)
|
||||||
- "Will I need an umbrella?" (location + weather, but could also web-search venue for indoor/outdoor)
|
- "Will I need an umbrella?" (location + weather, but could also web-search venue for indoor/outdoor)
|
||||||
- "What should I know before my meeting with Acme Corp?" (calendar + web search for company info)
|
- "What should I know before my meeting with Acme Corp?" (calendar + web search for company info)
|
||||||
@@ -879,10 +888,12 @@ This is also where intent extraction happens for the Gentle Follow-up Agent. Eve
|
|||||||
The backbone for general knowledge. Makes AELIS a person you can ask things, not just a dashboard you look at.
|
The backbone for general knowledge. Makes AELIS a person you can ask things, not just a dashboard you look at.
|
||||||
|
|
||||||
**Reactive (user asks):**
|
**Reactive (user asks):**
|
||||||
|
|
||||||
- Recipe ideas, how-to questions, factual lookups, recommendations
|
- Recipe ideas, how-to questions, factual lookups, recommendations
|
||||||
- Anything the source graph can't answer
|
- Anything the source graph can't answer
|
||||||
|
|
||||||
**Proactive (agents trigger):**
|
**Proactive (agents trigger):**
|
||||||
|
|
||||||
- Contextual Preparation enriches calendar events: venue info, attendee backgrounds, parking
|
- Contextual Preparation enriches calendar events: venue info, attendee backgrounds, parking
|
||||||
- Feed shows a concert → pre-fetches setlist, venue details
|
- Feed shows a concert → pre-fetches setlist, venue details
|
||||||
- Ambient Context checks for disruptions, closures, news
|
- Ambient Context checks for disruptions, closures, news
|
||||||
@@ -949,7 +960,7 @@ Handles tasks the user delegates via natural language.
|
|||||||
|
|
||||||
Requires write access to sources. Confirmation UX for anything destructive or costly.
|
Requires write access to sources. Confirmation UX for anything destructive or costly.
|
||||||
|
|
||||||
**Implementation:** Extends the Query Agent. When the LLM determines the user wants to *do* something (not just ask), it calls a delegation tool with structured output: `{ action: "create_reminder" | "schedule_meeting" | "add_task", params: {...} }`. The backend maps this to `executeAction()` on the relevant source. For "find a time that works for both me and Sarah," the agent queries both calendars (requires Sarah to be a known contact with calendar access — or the agent asks the user to share availability). All write actions go through a confirmation step: the backend sends a `delegation.confirm` notification with the proposed action, and the client shows a confirmation UI. The user approves or modifies before execution. Store delegation history for the Follow-up Agent.
|
**Implementation:** Extends the Query Agent. When the LLM determines the user wants to _do_ something (not just ask), it calls a delegation tool with structured output: `{ action: "create_reminder" | "schedule_meeting" | "add_task", params: {...} }`. The backend maps this to `executeAction()` on the relevant source. For "find a time that works for both me and Sarah," the agent queries both calendars (requires Sarah to be a known contact with calendar access — or the agent asks the user to share availability). All write actions go through a confirmation step: the backend sends a `delegation.confirm` notification with the proposed action, and the client shows a confirmation UI. The user approves or modifies before execution. Store delegation history for the Follow-up Agent.
|
||||||
|
|
||||||
#### Actions
|
#### Actions
|
||||||
|
|
||||||
|
|||||||
@@ -238,20 +238,15 @@ The user never waits for the LLM. They see the feed instantly with the previous
|
|||||||
The harness serializes items with their unfilled slots into a single prompt. Items without slots are excluded. The LLM sees everything at once and fills whatever slots are relevant.
|
The harness serializes items with their unfilled slots into a single prompt. Items without slots are excluded. The LLM sees everything at once and fills whatever slots are relevant.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function buildHarnessInput(
|
function buildHarnessInput(items: FeedItem[], context: AgentContext): HarnessInput {
|
||||||
items: FeedItem[],
|
|
||||||
context: AgentContext,
|
|
||||||
): HarnessInput {
|
|
||||||
const itemsWithSlots = items
|
const itemsWithSlots = items
|
||||||
.filter(item => item.slots && Object.keys(item.slots).length > 0)
|
.filter((item) => item.slots && Object.keys(item.slots).length > 0)
|
||||||
.map(item => ({
|
.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
data: item.data,
|
data: item.data,
|
||||||
slots: Object.fromEntries(
|
slots: Object.fromEntries(
|
||||||
Object.entries(item.slots!).map(
|
Object.entries(item.slots!).map(([name, slot]) => [name, slot.description]),
|
||||||
([name, slot]) => [name, slot.description]
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -280,7 +275,11 @@ The LLM sees:
|
|||||||
{
|
{
|
||||||
"id": "calendar-event-456",
|
"id": "calendar-event-456",
|
||||||
"type": "calendar-event",
|
"type": "calendar-event",
|
||||||
"data": { "title": "Dinner at The Ivy", "startTime": "19:00", "location": "The Ivy, West St" },
|
"data": {
|
||||||
|
"title": "Dinner at The Ivy",
|
||||||
|
"startTime": "19:00",
|
||||||
|
"location": "The Ivy, West St"
|
||||||
|
},
|
||||||
"slots": {
|
"slots": {
|
||||||
"context": "Background on this event, attendees, or previous meetings with these people",
|
"context": "Background on this event, attendees, or previous meetings with these people",
|
||||||
"logistics": "Travel time, parking, directions to the venue",
|
"logistics": "Travel time, parking, directions to the venue",
|
||||||
@@ -315,7 +314,10 @@ A flat map of item ID → slot name → text content. Slots left null are unfill
|
|||||||
"id": "briefing-morning",
|
"id": "briefing-morning",
|
||||||
"type": "briefing",
|
"type": "briefing",
|
||||||
"data": {},
|
"data": {},
|
||||||
"ui": { "component": "Text", "props": { "text": "Light afternoon — just your dinner at 7. Rain clears by then." } }
|
"ui": {
|
||||||
|
"component": "Text",
|
||||||
|
"props": { "text": "Light afternoon — just your dinner at 7. Rain clears by then." }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"suppress": [],
|
"suppress": [],
|
||||||
@@ -333,10 +335,7 @@ class EnhancementManager {
|
|||||||
private lastInputHash: string | null = null
|
private lastInputHash: string | null = null
|
||||||
private running = false
|
private running = false
|
||||||
|
|
||||||
async enhance(
|
async enhance(items: FeedItem[], context: AgentContext): Promise<EnhancementResult> {
|
||||||
items: FeedItem[],
|
|
||||||
context: AgentContext,
|
|
||||||
): Promise<EnhancementResult> {
|
|
||||||
const hash = computeHash(items, context)
|
const hash = computeHash(items, context)
|
||||||
|
|
||||||
if (hash === this.lastInputHash && this.cache) {
|
if (hash === this.lastInputHash && this.cache) {
|
||||||
@@ -349,12 +348,14 @@ class EnhancementManager {
|
|||||||
|
|
||||||
this.running = true
|
this.running = true
|
||||||
this.runHarness(items, context)
|
this.runHarness(items, context)
|
||||||
.then(result => {
|
.then((result) => {
|
||||||
this.cache = result
|
this.cache = result
|
||||||
this.lastInputHash = hash
|
this.lastInputHash = hash
|
||||||
this.notifySubscribers(result)
|
this.notifySubscribers(result)
|
||||||
})
|
})
|
||||||
.finally(() => { this.running = false })
|
.finally(() => {
|
||||||
|
this.running = false
|
||||||
|
})
|
||||||
|
|
||||||
return this.cache ?? emptyResult()
|
return this.cache ?? emptyResult()
|
||||||
}
|
}
|
||||||
@@ -373,11 +374,8 @@ interface EnhancementResult {
|
|||||||
After the harness runs, the engine merges slot fills into items:
|
After the harness runs, the engine merges slot fills into items:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function mergeEnhancement(
|
function mergeEnhancement(items: FeedItem[], result: EnhancementResult): FeedItem[] {
|
||||||
items: FeedItem[],
|
return items.map((item) => {
|
||||||
result: EnhancementResult,
|
|
||||||
): FeedItem[] {
|
|
||||||
return items.map(item => {
|
|
||||||
const fills = result.slotFills[item.id]
|
const fills = result.slotFills[item.id]
|
||||||
if (!fills || !item.slots) return item
|
if (!fills || !item.slots) return item
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ The backend uses a raw `pg` Pool for Better Auth and has no ORM. We need a persi
|
|||||||
A `user_sources` table stores per-user source state:
|
A `user_sources` table stores per-user source state:
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| ------------ | ------------------------ | ------------------------------------------------------------ |
|
| ------------- | --------------------- | -------------------------------------------------------------- |
|
||||||
| `id` | `uuid` PK | Row ID |
|
| `id` | `uuid` PK | Row ID |
|
||||||
| `user_id` | `text` FK → `user.id` | Owner |
|
| `user_id` | `text` FK → `user.id` | Owner |
|
||||||
| `source_id` | `text` | Source identifier (e.g., `aelis.tfl`, `aelis.weather`) |
|
| `source_id` | `text` | Source identifier (e.g., `aelis.tfl`, `aelis.weather`) |
|
||||||
| `enabled` | `boolean` | Whether this source is active in the user's feed |
|
| `enabled` | `boolean` | Whether this source is active in the user's feed |
|
||||||
| `config` | `jsonb` | Source-specific configuration (validated by source at runtime)|
|
| `config` | `jsonb` | Source-specific configuration (validated by source at runtime) |
|
||||||
| `credentials`| `bytea` | Encrypted OAuth tokens / secrets (AES-256-GCM) |
|
| `credentials` | `bytea` | Encrypted OAuth tokens / secrets (AES-256-GCM) |
|
||||||
| `created_at` | `timestamp with tz` | Row creation time |
|
| `created_at` | `timestamp with tz` | Row creation time |
|
||||||
| `updated_at` | `timestamp with tz` | Last modification time |
|
| `updated_at` | `timestamp with tz` | Last modification time |
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ A `user_sources` table stores per-user source state:
|
|||||||
When a new user is created, seed `user_sources` rows for default sources:
|
When a new user is created, seed `user_sources` rows for default sources:
|
||||||
|
|
||||||
| Source | Default config |
|
| Source | Default config |
|
||||||
| ------------------ | --------------------------------------------------------------- |
|
| ---------------- | ----------------------------------------------------------- |
|
||||||
| `aelis.location` | `{}` |
|
| `aelis.location` | `{}` |
|
||||||
| `aelis.weather` | `{ "units": "metric", "hourlyLimit": 12, "dailyLimit": 7 }` |
|
| `aelis.weather` | `{ "units": "metric", "hourlyLimit": 12, "dailyLimit": 7 }` |
|
||||||
| `aelis.tfl` | `{ "lines": <all default lines> }` |
|
| `aelis.tfl` | `{ "lines": <all default lines> }` |
|
||||||
@@ -67,16 +67,22 @@ Each provider receives the Drizzle DB instance and queries `user_sources` intern
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class TflSourceProvider implements FeedSourceProvider {
|
class TflSourceProvider implements FeedSourceProvider {
|
||||||
constructor(private db: DrizzleDb, private apiKey: string) {}
|
constructor(
|
||||||
|
private db: DrizzleDb,
|
||||||
|
private apiKey: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
async feedSourceForUser(userId: string): Promise<TflSource> {
|
async feedSourceForUser(userId: string): Promise<TflSource> {
|
||||||
const row = await this.db.select()
|
const row = await this.db
|
||||||
|
.select()
|
||||||
.from(userSources)
|
.from(userSources)
|
||||||
.where(and(
|
.where(
|
||||||
|
and(
|
||||||
eq(userSources.userId, userId),
|
eq(userSources.userId, userId),
|
||||||
eq(userSources.sourceId, "aelis.tfl"),
|
eq(userSources.sourceId, "aelis.tfl"),
|
||||||
eq(userSources.enabled, true),
|
eq(userSources.enabled, true),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!row[0]) {
|
if (!row[0]) {
|
||||||
@@ -210,16 +216,19 @@ _`feed-source-provider.ts`, `user-session-manager.ts`, `engine/http.ts`, and `lo
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
**Add:**
|
**Add:**
|
||||||
|
|
||||||
- `drizzle-orm`
|
- `drizzle-orm`
|
||||||
- `drizzle-kit` (dev)
|
- `drizzle-kit` (dev)
|
||||||
|
|
||||||
**Remove:**
|
**Remove:**
|
||||||
|
|
||||||
- `pg`
|
- `pg`
|
||||||
- `@types/pg` (dev)
|
- `@types/pg` (dev)
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
**Add to `.env.example`:**
|
**Add to `.env.example`:**
|
||||||
|
|
||||||
- `CREDENTIALS_ENCRYPTION_KEY` — 32-byte hex or base64 key for AES-256-GCM
|
- `CREDENTIALS_ENCRYPTION_KEY` — 32-byte hex or base64 key for AES-256-GCM
|
||||||
|
|
||||||
## Open Questions (Deferred)
|
## Open Questions (Deferred)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Source IDs use reverse domain notation. Built-in sources use `aelis.<name>`. Thi
|
|||||||
Action IDs are descriptive verb-noun pairs in kebab-case, scoped to their source. The globally unique form is `<sourceId>/<actionId>`.
|
Action IDs are descriptive verb-noun pairs in kebab-case, scoped to their source. The globally unique form is `<sourceId>/<actionId>`.
|
||||||
|
|
||||||
| Source ID | Action IDs |
|
| Source ID | Action IDs |
|
||||||
| --------------- | -------------------------------------------------------------- |
|
| ---------------- | -------------------------------------------------------------- |
|
||||||
| `aelis.location` | `update-location` (migrated from `pushLocation()`) |
|
| `aelis.location` | `update-location` (migrated from `pushLocation()`) |
|
||||||
| `aelis.tfl` | `set-lines-of-interest` (migrated from `setLinesOfInterest()`) |
|
| `aelis.tfl` | `set-lines-of-interest` (migrated from `setLinesOfInterest()`) |
|
||||||
| `aelis.weather` | _(none)_ |
|
| `aelis.weather` | _(none)_ |
|
||||||
@@ -241,8 +241,16 @@ class SpotifySource implements FeedSource<SpotifyFeedItem> {
|
|||||||
type: "View",
|
type: "View",
|
||||||
className: "flex-1",
|
className: "flex-1",
|
||||||
children: [
|
children: [
|
||||||
{ type: "Text", className: "font-semibold text-black dark:text-white", text: track.name },
|
{
|
||||||
{ type: "Text", className: "text-sm text-gray-500 dark:text-gray-400", text: track.artist },
|
type: "Text",
|
||||||
|
className: "font-semibold text-black dark:text-white",
|
||||||
|
text: track.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Text",
|
||||||
|
className: "text-sm text-gray-500 dark:text-gray-400",
|
||||||
|
text: track.artist,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -261,8 +269,8 @@ class SpotifySource implements FeedSource<SpotifyFeedItem> {
|
|||||||
4. `FeedSource.listActions()` is a required method returning `Record<string, ActionDefinition>` (empty record if no actions)
|
4. `FeedSource.listActions()` is a required method returning `Record<string, ActionDefinition>` (empty record if no actions)
|
||||||
5. `FeedSource.executeAction()` is a required method (no-op for sources without actions)
|
5. `FeedSource.executeAction()` is a required method (no-op for sources without actions)
|
||||||
6. `FeedItem.actions` is an optional readonly array of `ItemAction`
|
6. `FeedItem.actions` is an optional readonly array of `ItemAction`
|
||||||
6b. `FeedItem.ui` is an optional json-render tree describing server-driven UI
|
6b. `FeedItem.ui` is an optional json-render tree describing server-driven UI
|
||||||
6c. `FeedItem.slots` is an optional record of named LLM-fillable slots
|
6c. `FeedItem.slots` is an optional record of named LLM-fillable slots
|
||||||
7. `FeedEngine.executeAction()` routes to correct source, returns `ActionResult`
|
7. `FeedEngine.executeAction()` routes to correct source, returns `ActionResult`
|
||||||
8. `FeedEngine.listActions()` aggregates actions from all sources
|
8. `FeedEngine.listActions()` aggregates actions from all sources
|
||||||
9. Existing tests pass unchanged (all changes are additive)
|
9. Existing tests pass unchanged (all changes are additive)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test ."
|
"test": "bun test ."
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@nym.sh/jrx": "*",
|
|
||||||
"@json-render/core": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.1.0"
|
"@standard-schema/spec": "^1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@json-render/core": "*",
|
||||||
|
"@nym.sh/jrx": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import type { Context, FeedEnhancement, FeedItem, FeedPostProcessor } from "@aelis/core"
|
import type { Context, FeedEnhancement, FeedItem, FeedPostProcessor } from "@aelis/core"
|
||||||
|
|
||||||
import { TimeRelevance } from "@aelis/core"
|
|
||||||
|
|
||||||
import type { CalDavEventData } from "@aelis/source-caldav"
|
import type { CalDavEventData } from "@aelis/source-caldav"
|
||||||
import type { CalendarEventData } from "@aelis/source-google-calendar"
|
import type { CalendarEventData } from "@aelis/source-google-calendar"
|
||||||
import type { CurrentWeatherData } from "@aelis/source-weatherkit"
|
import type { CurrentWeatherData } from "@aelis/source-weatherkit"
|
||||||
|
|
||||||
|
import { TimeRelevance } from "@aelis/core"
|
||||||
import { CalDavFeedItemType } from "@aelis/source-caldav"
|
import { CalDavFeedItemType } from "@aelis/source-caldav"
|
||||||
import { CalendarFeedItemType } from "@aelis/source-google-calendar"
|
import { CalendarFeedItemType } from "@aelis/source-google-calendar"
|
||||||
import { TflFeedItemType } from "@aelis/source-tfl"
|
import { TflFeedItemType } from "@aelis/source-tfl"
|
||||||
import { WeatherFeedItemType } from "@aelis/source-weatherkit"
|
import { WeatherFeedItemType } from "@aelis/source-weatherkit"
|
||||||
|
|
||||||
|
|
||||||
export const TimePeriod = {
|
export const TimePeriod = {
|
||||||
Morning: "morning",
|
Morning: "morning",
|
||||||
Afternoon: "afternoon",
|
Afternoon: "afternoon",
|
||||||
@@ -28,7 +25,6 @@ export const DayType = {
|
|||||||
|
|
||||||
export type DayType = (typeof DayType)[keyof typeof DayType]
|
export type DayType = (typeof DayType)[keyof typeof DayType]
|
||||||
|
|
||||||
|
|
||||||
const PRE_MEETING_WINDOW_MS = 30 * 60 * 1000
|
const PRE_MEETING_WINDOW_MS = 30 * 60 * 1000
|
||||||
const TRANSITION_WINDOW_MS = 30 * 60 * 1000
|
const TRANSITION_WINDOW_MS = 30 * 60 * 1000
|
||||||
|
|
||||||
@@ -144,7 +140,6 @@ export function createTimeOfDayEnhancer(options?: TimeOfDayEnhancerOptions): Fee
|
|||||||
return timeOfDayEnhancer
|
return timeOfDayEnhancer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getTimePeriod(date: Date): TimePeriod {
|
export function getTimePeriod(date: Date): TimePeriod {
|
||||||
const hour = date.getHours()
|
const hour = date.getHours()
|
||||||
if (hour >= 22 || hour < 6) return TimePeriod.Night
|
if (hour >= 22 || hour < 6) return TimePeriod.Night
|
||||||
@@ -182,7 +177,9 @@ function getNextPeriodBoundary(date: Date): { period: TimePeriod; msUntil: numbe
|
|||||||
* Google Calendar uses `startTime`, CalDAV uses `startDate`.
|
* Google Calendar uses `startTime`, CalDAV uses `startDate`.
|
||||||
*/
|
*/
|
||||||
function getEventStartTime(data: CalendarEventData | CalDavEventData): Date {
|
function getEventStartTime(data: CalendarEventData | CalDavEventData): Date {
|
||||||
return "startTime" in data ? (data as CalendarEventData).startTime : (data as CalDavEventData).startDate
|
return "startTime" in data
|
||||||
|
? (data as CalendarEventData).startTime
|
||||||
|
: (data as CalDavEventData).startDate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,7 +193,6 @@ function hasPrecipitationOrExtreme(item: FeedItem): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface PreMeetingInfo {
|
interface PreMeetingInfo {
|
||||||
/** IDs of calendar items starting within the pre-meeting window */
|
/** IDs of calendar items starting within the pre-meeting window */
|
||||||
upcomingMeetingIds: Set<string>
|
upcomingMeetingIds: Set<string>
|
||||||
@@ -225,7 +221,6 @@ function detectPreMeetingItems(items: FeedItem[], now: Date): PreMeetingInfo {
|
|||||||
return { upcomingMeetingIds, hasLocationMeeting }
|
return { upcomingMeetingIds, hasLocationMeeting }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function findFirstEventOfDay(items: FeedItem[], now: Date): string | null {
|
function findFirstEventOfDay(items: FeedItem[], now: Date): string | null {
|
||||||
let earliest: { id: string; time: number } | null = null
|
let earliest: { id: string; time: number } | null = null
|
||||||
|
|
||||||
@@ -252,7 +247,6 @@ function findFirstEventOfDay(items: FeedItem[], now: Date): string | null {
|
|||||||
return earliest?.id ?? null
|
return earliest?.id ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyMorningWeekday(
|
function applyMorningWeekday(
|
||||||
items: FeedItem[],
|
items: FeedItem[],
|
||||||
boost: Record<string, number>,
|
boost: Record<string, number>,
|
||||||
@@ -415,7 +409,6 @@ function applyNight(items: FeedItem[], boost: Record<string, number>, suppress:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyPreMeetingOverrides(
|
function applyPreMeetingOverrides(
|
||||||
items: FeedItem[],
|
items: FeedItem[],
|
||||||
preMeeting: PreMeetingInfo,
|
preMeeting: PreMeetingInfo,
|
||||||
@@ -487,7 +480,6 @@ function applyWindDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyTransitionLookahead(
|
function applyTransitionLookahead(
|
||||||
items: FeedItem[],
|
items: FeedItem[],
|
||||||
now: Date,
|
now: Date,
|
||||||
@@ -544,7 +536,6 @@ function getNextPeriodBoostTargets(period: TimePeriod, dayType: DayType): Readon
|
|||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyWeatherTimeCorrelation(
|
function applyWeatherTimeCorrelation(
|
||||||
items: FeedItem[],
|
items: FeedItem[],
|
||||||
period: TimePeriod,
|
period: TimePeriod,
|
||||||
@@ -562,7 +553,11 @@ function applyWeatherTimeCorrelation(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case WeatherFeedItemType.Current:
|
case WeatherFeedItemType.Current:
|
||||||
if (period === TimePeriod.Morning && dayType === DayType.Weekday && hasPrecipitationOrExtreme(item)) {
|
if (
|
||||||
|
period === TimePeriod.Morning &&
|
||||||
|
dayType === DayType.Weekday &&
|
||||||
|
hasPrecipitationOrExtreme(item)
|
||||||
|
) {
|
||||||
boost[item.id] = (boost[item.id] ?? 0) + 0.1
|
boost[item.id] = (boost[item.id] ?? 0) + 0.1
|
||||||
}
|
}
|
||||||
if (period === TimePeriod.Evening && hasEveningEventWithLocation) {
|
if (period === TimePeriod.Evening && hasEveningEventWithLocation) {
|
||||||
@@ -591,5 +586,3 @@ function hasEveningCalendarEventWithLocation(items: FeedItem[], now: Date): bool
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,10 @@
|
|||||||
* Writes feed items (with slots) to scripts/.cache/feed-items.json for inspection.
|
* Writes feed items (with slots) to scripts/.cache/feed-items.json for inspection.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Context } from "@aelis/core"
|
||||||
import { mkdirSync, writeFileSync } from "node:fs"
|
import { mkdirSync, writeFileSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
import { Context } from "@aelis/core"
|
|
||||||
|
|
||||||
import { CalDavSource } from "../src/index.ts"
|
import { CalDavSource } from "../src/index.ts"
|
||||||
|
|
||||||
const serverUrl = prompt("CalDAV server URL:")
|
const serverUrl = prompt("CalDAV server URL:")
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"fetch-fixtures": "bun run scripts/fetch-fixtures.ts"
|
"fetch-fixtures": "bun run scripts/fetch-fixtures.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aelis/core": "workspace:*",
|
|
||||||
"@aelis/components": "workspace:*",
|
"@aelis/components": "workspace:*",
|
||||||
|
"@aelis/core": "workspace:*",
|
||||||
"@aelis/source-location": "workspace:*",
|
"@aelis/source-location": "workspace:*",
|
||||||
"arktype": "^2.1.0"
|
"arktype": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,15 +9,14 @@
|
|||||||
* Usage: bun packages/aelis-source-weatherkit/scripts/query.ts
|
* Usage: bun packages/aelis-source-weatherkit/scripts/query.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Context } from "@aelis/core"
|
||||||
|
import { LocationKey } from "@aelis/source-location"
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { createInterface } from "node:readline/promises"
|
import { createInterface } from "node:readline/promises"
|
||||||
|
|
||||||
import { Context } from "@aelis/core"
|
|
||||||
import { LocationKey } from "@aelis/source-location"
|
|
||||||
|
|
||||||
import { DefaultWeatherKitClient } from "../src/weatherkit"
|
|
||||||
import { WeatherSource, Units } from "../src/weather-source"
|
import { WeatherSource, Units } from "../src/weather-source"
|
||||||
|
import { DefaultWeatherKitClient } from "../src/weatherkit"
|
||||||
|
|
||||||
const SCRIPT_DIR = import.meta.dirname
|
const SCRIPT_DIR = import.meta.dirname
|
||||||
const CACHE_DIR = join(SCRIPT_DIR, ".cache")
|
const CACHE_DIR = join(SCRIPT_DIR, ".cache")
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { Context } from "@aelis/core"
|
|||||||
import { LocationKey } from "@aelis/source-location"
|
import { LocationKey } from "@aelis/source-location"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
import type { WeatherKitClient, WeatherKitResponse, HourlyForecast, DailyForecast } from "./weatherkit"
|
import type {
|
||||||
|
WeatherKitClient,
|
||||||
|
WeatherKitResponse,
|
||||||
|
HourlyForecast,
|
||||||
|
DailyForecast,
|
||||||
|
} from "./weatherkit"
|
||||||
|
|
||||||
import fixture from "../fixtures/san-francisco.json"
|
import fixture from "../fixtures/san-francisco.json"
|
||||||
import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items"
|
import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items"
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from
|
|||||||
import { Context, TimeRelevance, UnknownActionError } from "@aelis/core"
|
import { Context, TimeRelevance, UnknownActionError } from "@aelis/core"
|
||||||
import { LocationKey } from "@aelis/source-location"
|
import { LocationKey } from "@aelis/source-location"
|
||||||
|
|
||||||
import { WeatherFeedItemType, type DailyWeatherEntry, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items"
|
import {
|
||||||
|
WeatherFeedItemType,
|
||||||
|
type DailyWeatherEntry,
|
||||||
|
type HourlyWeatherEntry,
|
||||||
|
type WeatherFeedItem,
|
||||||
|
} from "./feed-items"
|
||||||
import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt"
|
import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt"
|
||||||
import { WeatherKey, type Weather } from "./weather-context"
|
import { WeatherKey, type Weather } from "./weather-context"
|
||||||
import {
|
import {
|
||||||
|
|||||||
Reference in New Issue
Block a user