chore(*): migrate drizzle-duckdb-wasm and duckdb-wasm to proj-airi/duckdb-wasm (#116)

* chore(drizzle-duckdb-wasm|duckdb-wasm): clean up packages
* chore(realtime-audio): remove unused paths from tsconfig
* chore(*): update dependencies
This commit is contained in:
Makito
2025-04-05 18:14:15 +08:00
committed by GitHub
parent 15fa1fcfbe
commit 00c30fbdbc
71 changed files with 126 additions and 10427 deletions
-8
View File
@@ -10,14 +10,6 @@
],
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@proj-airi/duckdb-wasm/*": [
"../duckdb-wasm/src/*"
],
"@proj-airi/duckdb-wasm": [
"../duckdb-wasm/src/index.ts"
]
},
"types": [
"vite/client",
"@vitest/browser/providers/playwright",
+1 -1
View File
@@ -92,7 +92,7 @@
"@iconify-json/vscode-icons": "^1.2.18",
"@iconify/utils": "^2.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.5",
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
"@proj-airi/lobe-icons": "^1.0.5",
"@proj-airi/provider-transformers": "workspace:^",
"@proj-airi/ui-transitions": "workspace:^",
+1 -1
View File
@@ -37,7 +37,7 @@
"@pixiv/three-vrm-animation": "^3.3.6",
"@pixiv/three-vrm-core": "^3.3.6",
"@proj-airi/ccc": "workspace:^",
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
"@proj-airi/provider-transformers": "workspace:^",
"@proj-airi/server-sdk": "workspace:^",
"@proj-airi/stage-ui": "workspace:^",
+2 -138
View File
@@ -1,139 +1,3 @@
# 🦆 Drizzle ORM driver for `@duckdb/duckdb-wasm`
# We have moved
This package provides a Drizzle ORM driver for the DuckDB WASM implementation.
> [Playground](https://drizzle-orm-duckdb-wasm.netlify.app/)
## Installation
Pick the package manager of your choice:
```shell
ni @proj-airi/drizzle-duckdb-wasm -D # from @antfu/ni, can be installed via `npm i -g @antfu/ni`
pnpm i @proj-airi/drizzle-duckdb-wasm -D
yarn i @proj-airi/drizzle-duckdb-wasm -D
npm i @proj-airi/drizzle-duckdb-wasm -D
```
## Usage
```typescript
import { drizzle } from '@proj-airi/drizzle-duckdb-wasm'
const db = drizzle('duckdb-wasm://?bundles=import-url', { schema })
```
### Browser
#### Vue.js
```typescript
// ./db/schema.ts
import { sql } from 'drizzle-orm'
import { pgTable, uuid } from 'drizzle-orm/pg-core'
export const users = pgTable('users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
}))
```
```shell
drizzle-kit generate
```
```html
<!-- ./src/App.vue -->
<script setup lang="ts">
import type { DuckDBWasmDrizzleDatabase } from '@proj-airi/drizzle-duckdb-wasm'
import { drizzle } from '@proj-airi/drizzle-duckdb-wasm'
import { getImportUrlBundles } from '@proj-airi/drizzle-duckdb-wasm/bundles/import-url-browser'
import { useDebounceFn } from '@vueuse/core'
import { serialize } from 'superjson'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as schema from './db/schema'
import { users } from './db/schema'
import migration1 from './drizzle/0000_cute_kulan_gath.sql?raw'
const results = ref<any[]>([])
onMounted(async () => {
// db.value = drizzle('duckdb-wasm://?bundles=import-url', { schema })
// db.value = drizzle({ connection: { bundles: getImportUrlBundles() } }, { schema })
await db.value?.execute(migration1)
results.value = await db.value?.execute('SELECT count(*)::INTEGER as v FROM generate_series(0, 100) t(v)')
console.log(results.value) // Output [{ v: 101 }]
await db.value.insert(users).values({ id: '00000000-0000-0000-0000-000000000000' })
const foundUsers = await db.value.select().from(users)
console.log(foundUsers) // Output [{ id: '00000000-0000-0000-0000-000000000000' }]
})
onUnmounted(async () => {
if (db.value) {
const client = await db.value.$client
await client.close()
}
})
</script>
```
### Node.js
You will need to install `web-worker` too.
```shell
ni web-worker # from @antfu/ni, can be installed via `npm i -g @antfu/ni`
pnpm i web-worker
yarn i web-worker
npm i web-worker
```
```typescript
// ./db/schema.ts
import { sql } from 'drizzle-orm'
import { pgTable, uuid } from 'drizzle-orm/pg-core'
export const users = pgTable('users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
}))
```
```shell
drizzle-kit generate
```
```typescript
// ./src/index.ts
import { read } from 'node:fs/promises'
import { drizzle } from '@proj-airi/drizzle-duckdb-wasm'
import { getBundles } from '@proj-airi/drizzle-duckdb-wasm/bundles/default-node'
import * as schema from './db/schema'
import { users } from './db/schema'
async function main() {
const db = drizzle({ connection: { bundles: getBundles() } }, { schema })
// Run migration scripts
const migration1 = await read('./drizzle/0000_cute_kulan_gath.sql', 'utf-8')
await db.execute(migration1)
const res = await db.execute('SELECT count(*)::INTEGER as v FROM generate_series(0, 100) t(v)')
console.log(res) // Output [{ v: 101 }]
await db.insert(users).values({ id: '00000000-0000-0000-0000-000000000000' })
const foundUsers = await db.select().from(users)
console.log(foundUsers) // Output [{ id: '00000000-0000-0000-0000-000000000000' }]
// Remember to close it when you are done
const client = await db.$client
await client.close()
}
```
## Footnotes
Check out [the package](https://github.com/moeru-ai/airi/tree/main/packages/duckdb-wasm/README.md) we made for easier call to `@duckdb/duckdb-wasm` as well!
Hello! This package has been moved to [proj-airi/duckdb-wasm](https://github.com/proj-airi/duckdb-wasm). You may keep tracking the updates of this package there.
@@ -1,7 +0,0 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'postgresql',
schema: './playground/db/schema.ts',
out: './playground/drizzle',
})
-23
View File
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Project AIRI Memory Driver @duckdb/duckdb-wasm Playground</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script>
;(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
</head>
<body class="font-sans">
<div id="app"></div>
<script type="module" src="/playground/src/main.ts"></script>
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
</body>
</html>
-13
View File
@@ -1,13 +0,0 @@
[build]
base = "/"
command = "pnpm -F @proj-airi/drizzle-duckdb-wasm... run build"
publish = "/packages/drizzle-duckdb-wasm/playground/dist"
[build.environment]
NODE_VERSION = "23"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
force = false
-99
View File
@@ -1,99 +0,0 @@
{
"name": "@proj-airi/drizzle-duckdb-wasm",
"type": "module",
"version": "0.4.19",
"description": "🦆 Drizzle ORM driver for @duckdb/duckdb-wasm that works on both browser and Node.js environments",
"author": {
"name": "Neko Ayaka",
"email": "neko@ayaka.moe",
"url": "https://github.com/nekomeowww"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/drizzle-duckdb-wasm"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"node": "./dist/index.cjs"
},
"./bundles/default-browser": {
"types": "./dist/bundles/default-browser.d.ts",
"import": "./dist/bundles/default-browser.mjs"
},
"./bundles/default-node": {
"types": "./dist/bundles/default-node.d.ts",
"import": "./dist/bundles/default-node.mjs",
"node": "./dist/bundles/default-node.cjs"
},
"./bundles/import-url-browser": {
"types": "./dist/bundles/import-url-browser.d.ts",
"import": "./dist/bundles/import-url-browser.mjs"
},
"./bundles/import-url-node": {
"types": "./dist/bundles/import-url-node.d.ts",
"import": "./dist/bundles/import-url-node.mjs",
"node": "./dist/bundles/import-url-node.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "pnpm run stub",
"stub": "unbuild",
"build": "unbuild && pnpm run play:build",
"play:dev": "vite",
"play:build": "vite build",
"play:preview": "vite preview",
"typecheck": "vue-tsc --noEmit",
"db:generate": "drizzle-kit generate",
"test": "vitest",
"test:run": "vitest run"
},
"peerDependencies": {
"web-worker": "^1.5.0"
},
"peerDependenciesMeta": {
"web-worker": {
"optional": true
}
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@duckdb/duckdb-wasm": "1.29.1-dev68.0",
"@proj-airi/duckdb-wasm": "workspace:^",
"apache-arrow": "^19.0.1",
"date-fns": "^4.1.0",
"defu": "^6.1.4",
"drizzle-orm": "^0.41.0",
"es-toolkit": "^1.34.1"
},
"devDependencies": {
"@iconify-json/solar": "^1.2.2",
"@types/d3": "^7.4.3",
"@types/d3-force": "^3.0.10",
"@unocss/reset": "^66.1.0-beta.9",
"@vitejs/plugin-vue": "^5.2.3",
"@vitest/browser": "^3.1.1",
"@vueuse/core": "^13.0.0",
"d3": "^7.9.0",
"d3-force": "^3.0.0",
"drizzle-kit": "^0.30.6",
"playwright": "^1.51.1",
"superjson": "^2.2.2",
"unplugin-vue-router": "^0.12.0",
"vite": "^6.2.5",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tsc": "^3.0.0-alpha.2"
}
}
@@ -1,86 +0,0 @@
import { sql } from 'drizzle-orm'
import {
bigint,
// bit,
boolean,
char,
date,
decimal,
doublePrecision,
integer,
interval,
json,
numeric,
pgTable,
real,
smallint,
text,
time,
timestamp,
uuid,
varchar,
vector,
} from 'drizzle-orm/pg-core'
export const users = pgTable('users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
int16: smallint().notNull().default(0),
int32: integer().notNull().default(0),
int64: bigint({ mode: 'number' }).notNull().default(sql`0`),
int64BigInt: bigint({ mode: 'bigint' }).notNull().default(sql`0`),
decimal: decimal().notNull().default(sql`0`),
numeric: numeric().notNull().default(sql`0`),
real: real().notNull().default(sql`0`),
double: doublePrecision().notNull().default(sql`0`),
vector: vector({ dimensions: 4 }).notNull().default(sql`[0,0,0,0]`),
// Unimplemented type for cast (INTEGER[] -> BIT) from @duckdb/duckdb-wasm
// bit: bit({ dimensions: 4 }).notNull().default(sql`[0,0,0,0]`),
bool: boolean().notNull().default(false),
char: char().notNull().default(''),
varchar: varchar().notNull().default(''),
text: text().notNull().default(''),
json: json().notNull().default(sql`'{}'`),
date: date().notNull().default(sql`'2020-01-01'`),
time: time().notNull().default(sql`'00:00:00'`),
timestamp: timestamp().notNull().default(sql`'2020-01-01 00:00:00'`),
interval: interval().notNull().default(sql`'1 day'`),
}))
export const nodeUsers = pgTable('node_users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
name: text().notNull().default(''),
}))
export const nodePets = pgTable('node_pets', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
name: text().notNull().default(''),
}))
export const nodeGroups = pgTable('node_groups', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
name: text().notNull().default(''),
}))
export const edgePets = pgTable('edge_pets', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
source: uuid(),
target: uuid(),
}))
export const edgeOwners = pgTable('edge_owners', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
source: uuid(),
target: uuid(),
}))
export const edgeUsers = pgTable('edge_users', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
source: uuid(),
target: uuid(),
}))
export const edgeGroups = pgTable('edge_groups', () => ({
id: uuid().primaryKey().unique().default(sql`gen_random_uuid()`),
source: uuid(),
target: uuid(),
}))
@@ -1,22 +0,0 @@
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"int16" smallint DEFAULT 0 NOT NULL,
"int32" integer DEFAULT 0 NOT NULL,
"int64" bigint DEFAULT 0 NOT NULL,
"int64BigInt" bigint DEFAULT 0 NOT NULL,
"decimal" numeric DEFAULT 0 NOT NULL,
"numeric" numeric DEFAULT 0 NOT NULL,
"real" real DEFAULT 0 NOT NULL,
"double" double precision DEFAULT 0 NOT NULL,
"vector" FLOAT[4] DEFAULT [0,0,0,0] NOT NULL,
"bool" boolean DEFAULT false NOT NULL,
"char" char DEFAULT '' NOT NULL,
"varchar" varchar DEFAULT '' NOT NULL,
"text" text DEFAULT '' NOT NULL,
"json" json DEFAULT '{}' NOT NULL,
"date" date DEFAULT '2020-01-01' NOT NULL,
"time" time DEFAULT '00:00:00' NOT NULL,
"timestamp" timestamp DEFAULT '2020-01-01 00:00:00' NOT NULL,
"interval" interval DEFAULT '1 day' NOT NULL,
CONSTRAINT "users_id_unique" UNIQUE("id")
);
@@ -1,45 +0,0 @@
CREATE TABLE "edge_groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source" uuid,
"target" uuid,
CONSTRAINT "edge_groups_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "edge_owners" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source" uuid,
"target" uuid,
CONSTRAINT "edge_owners_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "edge_pets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source" uuid,
"target" uuid,
CONSTRAINT "edge_pets_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "edge_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source" uuid,
"target" uuid,
CONSTRAINT "edge_users_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "node_groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text DEFAULT '' NOT NULL,
CONSTRAINT "node_groups_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "node_pets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text DEFAULT '' NOT NULL,
CONSTRAINT "node_pets_id_unique" UNIQUE("id")
);
--> statement-breakpoint
CREATE TABLE "node_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text DEFAULT '' NOT NULL,
CONSTRAINT "node_users_id_unique" UNIQUE("id")
);
@@ -1,173 +0,0 @@
{
"id": "5305094b-45db-4a61-a74f-340b2b7da250",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"int16": {
"name": "int16",
"type": "smallint",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int32": {
"name": "int32",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int64": {
"name": "int64",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"int64BigInt": {
"name": "int64BigInt",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"decimal": {
"name": "decimal",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"numeric": {
"name": "numeric",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"real": {
"name": "real",
"type": "real",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"double": {
"name": "double",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"vector": {
"name": "vector",
"type": "vector(4)",
"primaryKey": false,
"notNull": true,
"default": "[0,0,0,0]"
},
"bool": {
"name": "bool",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"char": {
"name": "char",
"type": "char",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"varchar": {
"name": "varchar",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"text": {
"name": "text",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"json": {
"name": "json",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01'"
},
"time": {
"name": "time",
"type": "time",
"primaryKey": false,
"notNull": true,
"default": "'00:00:00'"
},
"timestamp": {
"name": "timestamp",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01 00:00:00'"
},
"interval": {
"name": "interval",
"type": "interval",
"primaryKey": false,
"notNull": true,
"default": "'0'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_id_unique": {
"name": "users_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -1,438 +0,0 @@
{
"id": "f64005fe-f9b9-4673-84cb-a24695abff11",
"prevId": "5305094b-45db-4a61-a74f-340b2b7da250",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.edge_groups": {
"name": "edge_groups",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"source": {
"name": "source",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"target": {
"name": "target",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"edge_groups_id_unique": {
"name": "edge_groups_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.edge_owners": {
"name": "edge_owners",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"source": {
"name": "source",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"target": {
"name": "target",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"edge_owners_id_unique": {
"name": "edge_owners_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.edge_pets": {
"name": "edge_pets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"source": {
"name": "source",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"target": {
"name": "target",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"edge_pets_id_unique": {
"name": "edge_pets_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.edge_users": {
"name": "edge_users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"source": {
"name": "source",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"target": {
"name": "target",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"edge_users_id_unique": {
"name": "edge_users_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.node_groups": {
"name": "node_groups",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"node_groups_id_unique": {
"name": "node_groups_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.node_pets": {
"name": "node_pets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"node_pets_id_unique": {
"name": "node_pets_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.node_users": {
"name": "node_users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"node_users_id_unique": {
"name": "node_users_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"int16": {
"name": "int16",
"type": "smallint",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int32": {
"name": "int32",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"int64": {
"name": "int64",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"int64BigInt": {
"name": "int64BigInt",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"decimal": {
"name": "decimal",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"numeric": {
"name": "numeric",
"type": "numeric",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"real": {
"name": "real",
"type": "real",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"double": {
"name": "double",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": "0"
},
"vector": {
"name": "vector",
"type": "vector(4)",
"primaryKey": false,
"notNull": true,
"default": "[0,0,0,0]"
},
"bool": {
"name": "bool",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"char": {
"name": "char",
"type": "char",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"varchar": {
"name": "varchar",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"text": {
"name": "text",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"json": {
"name": "json",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{}'"
},
"date": {
"name": "date",
"type": "date",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01'"
},
"time": {
"name": "time",
"type": "time",
"primaryKey": false,
"notNull": true,
"default": "'00:00:00'"
},
"timestamp": {
"name": "timestamp",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "'2020-01-01 00:00:00'"
},
"interval": {
"name": "interval",
"type": "interval",
"primaryKey": false,
"notNull": true,
"default": "'1 day'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_id_unique": {
"name": "users_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -1,20 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1739121102268,
"tag": "0000_cute_kulan_gath",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1740841981623,
"tag": "0001_parched_silver_centurion",
"breakpoints": true
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="24" height="24"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"></path>
<path fill="#fd7f9c" d="M18.296 3.045a1 1 0 0 1 .657.652l.03.119l1.341 7.154q.154.826.148 1.63l-.012.4H21a1 1 0 0 1 .117 1.993L21 15h-.894q-.164.531-.392 1.033l-.16.33l1.07.856a1 1 0 0 1-1.146 1.634l-.103-.072l-.936-.749A8.43 8.43 0 0 1 12 21a8.42 8.42 0 0 1-6.25-2.755l-.19-.213l-.935.749a1 1 0 0 1-1.343-1.477l.093-.085l1.07-.856a9 9 0 0 1-.435-1.012L3.894 15H3a1 1 0 0 1-.117-1.993L3 13h.54a8.5 8.5 0 0 1 .069-1.619l.067-.411l1.341-7.154a1 1 0 0 1 1.598-.604l.092.08l2.414 2.415a1 1 0 0 0 .576.284L9.828 6h4.344a1 1 0 0 0 .608-.206l.099-.087l2.414-2.414a1 1 0 0 1 1.003-.248m-.93 3.003L16.293 7.12a3 3 0 0 1-2.121.88H9.828a3 3 0 0 1-2.12-.879L6.632 6.048l-.992 5.29A6.5 6.5 0 0 0 5.545 13H7a1 1 0 1 1 0 2h-.492a.998.998 0 0 1 .71 1.696l-.095.086A6.44 6.44 0 0 0 12 19a6.43 6.43 0 0 0 4.696-2.02l.18-.2a1 1 0 0 1 .616-1.78H17a1 1 0 1 1 0-2h1.455a6.5 6.5 0 0 0-.096-1.662zm-3.472 9.005a1 1 0 0 1-.447 1.342l-.553.276a2 2 0 0 1-1.788 0l-.553-.276a1 1 0 0 1 .894-1.79l.553.277l.553-.276a1 1 0 0 1 1.341.447M9.5 10a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3m5 0a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3"></path>
</g>
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,80 +0,0 @@
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { RouterLink, RouterView } from 'vue-router'
const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>
<template>
<div mx-auto max-w-screen-lg flex flex-col gap-2 p-4>
<header flex flex-row items-center justify-between>
<h1 text-2xl>
<a href="https://github.com/duckdb/duckdb-wasm">🦆 <code>@duckdb/duckdb-wasm</code></a> + <a
href="https://orm.drizzle.team/"
>Drizzle ORM</a>
Playground
</h1>
<div flex flex-row items-center gap-2>
<button text-lg @click="() => toggleDark()">
<div v-if="isDark" i-solar:moon-stars-bold-duotone />
<div v-else i-solar:sun-bold />
</button>
<a href="https://github.com/moeru-ai/airi/tree/main/packages/drizzle-duckdb-wasm">
<div i-simple-icons:github />
</a>
</div>
</header>
<nav bg="neutral-100 dark:neutral-800" w-fit flex items-center of-hidden rounded-lg>
<RouterLink
to="/" px-3 py-2 bg="hover:neutral-200 dark:hover:neutral-700"
transition="all duration-250 ease-in-out"
>
<h1>Interactive</h1>
</RouterLink>
<div bg="neutral-200 dark:neutral-600" h="1lh" w="0.5" />
<RouterLink
to="/graph" px-3 py-2 bg="hover:neutral-200 dark:hover:neutral-700"
transition="all duration-250 ease-in-out"
>
<h1>Graph</h1>
</RouterLink>
<div bg="neutral-200 dark:neutral-600" h="1lh" w="0.5" />
<RouterLink
to="/memory-decay" px-3 py-2 bg="hover:neutral-200 dark:hover:neutral-700"
transition="all duration-250 ease-in-out"
>
<h1>Memory Decay</h1>
</RouterLink>
<div bg="neutral-200 dark:neutral-600" h="1lh" w="0.5" />
<RouterLink
to="/memory-simulator" px-3 py-2 bg="hover:neutral-200 dark:hover:neutral-700"
transition="all duration-250 ease-in-out"
>
<h1>Memory Simulate</h1>
</RouterLink>
</nav>
<RouterView />
</div>
</template>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
overscroll-behavior: none;
}
html {
background: #fff;
transition: all 0.3s ease-in-out;
}
html.dark {
background: #121212;
color-scheme: dark;
}
</style>
@@ -1,48 +0,0 @@
<script setup lang="ts">
import { SwitchRoot, SwitchThumb } from 'reka-ui'
const modelValue = defineModel<boolean>({ required: true })
</script>
<template>
<SwitchRoot
v-model="modelValue"
transition="background duration-250 ease-in-out"
outline="focus-within:none"
flex="~"
border="neutral-300 dark:neutral-700 data-[state=checked]:primary-200 data-[state=unchecked]:neutral-300 focus-within:neutral-800"
bg="data-[state=checked]:primary-400 data-[state=unchecked]:neutral-300 data-[state=checked]:dark:primary-400/80 dark:data-[state=unchecked]:neutral-800"
relative h-7 w="12.5" rounded-full
shadow="sm focus-within:shadow-neutral-800 focus-within:[0_0_0_1px] "
>
<SwitchThumb
my-auto size-6
flex items-center justify-center
translate-x="0.5 data-[state=checked]:full"
rounded-full bg-white text-xs shadow-xl
transition="transform duration-250 ease-in-out"
will-change-transform
/>
</SwitchRoot>
</template>
<style scoped>
.dark [border~='dark\:neutral-700'] {
--un-border-opacity: 1;
border-color: rgb(64 64 64 / var(--un-border-opacity));
}
[border~='data-\[state\=checked\]\:primary-200'][data-state='checked'] {
--un-border-opacity: 1;
border-color: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0) / var(--un-border-opacity));
}
[bg~='data-\[state\=checked\]\:primary-400'][data-state='checked'] {
--un-bg-opacity: 1;
background-color: oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + 0) / var(--un-bg-opacity));
}
.dark [bg~='data-\[state\=checked\]\:dark\:primary-400\/80'][data-state='checked'] {
background-color: oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + 0) / 0.8);
}
</style>
@@ -1,193 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import Checkbox from '../Checkbox.vue'
import Range from '../Range.vue'
const longTermMemoryEnabled = defineModel<boolean>('longTermMemoryEnabled', { default: false })
const longTermMemoryThreshold = defineModel<number>('longTermMemoryThreshold', { default: 1 })
const longTermMemoryStability = defineModel<number>('longTermMemoryStability', { default: 0.5 })
const longTermMemoryVisualize = defineModel<boolean>('longTermMemoryVisualize', { default: false })
const retrievalBoost = defineModel<number>('retrievalBoost', { default: 0.5 })
const retrievalDecaySlowdown = defineModel<number>('retrievalDecaySlowdown', { default: 0.5 })
const decayRate = defineModel<number>('decayRate', { default: 0.01 })
const timeUnit = defineModel<string>('timeUnit', { default: 'days' })
const maxDaysToShow = defineModel<number>('maxDaysToShow', { default: 30 })
// Computed percent values for display
const retrievalBoostPercent = computed(() => (retrievalBoost.value * 100).toFixed(0))
const decaySlowdownPercent = computed(() => (retrievalDecaySlowdown.value * 100).toFixed(0))
</script>
<template>
<div>
<!-- Long-term Memory Model -->
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">
Long-Term Memory Model
</h2>
<Checkbox v-model="longTermMemoryEnabled" />
</div>
<p class="mb-4 text-sm text-neutral-600 dark:text-neutral-400">
Configure how memories transition from short-term to permanent long-term memory with repeated retrievals
</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="flex flex-col gap-2">
<label class="font-medium">Retrieval Threshold</label>
<div class="flex items-center gap-2">
<Range
v-model="longTermMemoryThreshold"
:min="1"
:max="20"
:step="1"
class="w-full"
:disabled="!longTermMemoryEnabled"
/>
<span class="w-10 text-right font-mono">{{ longTermMemoryThreshold }}</span>
</div>
<div class="text-xs text-neutral-500">
Retrievals needed to form stable long-term memory
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Stability Factor</label>
<div class="flex items-center gap-2">
<Range
v-model="longTermMemoryStability"
:min="0.01"
:max="0.9"
:step="0.01"
class="w-full"
:disabled="!longTermMemoryEnabled"
/>
<span class="w-16 text-right font-mono">{{ longTermMemoryStability.toFixed(2) }}</span>
</div>
<div class="text-xs text-neutral-500">
How quickly memories stabilize (lower = faster stabilization)
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Memory Model Settings</label>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2">
<Checkbox v-model="longTermMemoryVisualize" />
<label for="ltmVisualize" class="text-sm">Show LTM projection</label>
</div>
</div>
<div class="text-xs text-neutral-500">
Options for memory visualization
</div>
</div>
</div>
</div>
<!-- Retrieval Effect Settings -->
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
<h2 class="mb-2 text-lg font-semibold">
Retrieval Effect Settings
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex flex-col gap-2">
<label class="font-medium">Retrieval Boost Factor</label>
<div class="flex items-center gap-2">
<Range
v-model="retrievalBoost"
:min="0"
:max="1"
:step="0.05"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ retrievalBoostPercent }}%</span>
</div>
<div class="text-xs text-neutral-500">
How much each retrieval strengthens memory
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Decay Slowdown Factor</label>
<div class="flex items-center gap-2">
<Range
v-model="retrievalDecaySlowdown"
:min="0.1"
:max="1"
:step="0.05"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ decaySlowdownPercent }}%</span>
</div>
<div class="text-xs text-neutral-500">
How much retrievals slow future decay (lower = more slowdown)
</div>
</div>
</div>
</div>
<!-- General Decay Controls -->
<div class="grid grid-cols-1 mb-4 gap-4 rounded-lg bg-neutral-100 p-4 md:grid-cols-3 dark:bg-neutral-800/50">
<div class="flex flex-col gap-2">
<label class="font-medium">Base Decay Rate</label>
<div class="flex items-center gap-2">
<Range
v-model="decayRate"
:min="0.01"
:max="0.5"
:step="0.01"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ decayRate.toFixed(4) }}</span>
</div>
<div class="text-xs text-neutral-500">
Controls decay speed (0.0990 = ~10% per day)
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Time Unit</label>
<select
v-model="timeUnit"
class="w-full rounded-lg border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-700/30"
>
<option value="hours">
Hours
</option>
<option value="days">
Days
</option>
<option value="weeks">
Weeks
</option>
<option value="months">
Months
</option>
</select>
<div class="text-xs text-neutral-500">
Unit used in calculations (affects decay rate)
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Future Projection (Days)</label>
<div class="flex items-center gap-2">
<Range
v-model="maxDaysToShow"
:min="7"
:max="90"
:step="1"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ maxDaysToShow }}</span>
</div>
<div class="text-xs text-neutral-500">
How many days to project into the future
</div>
</div>
</div>
</div>
</template>
@@ -1,226 +0,0 @@
<!-- src/components/TimeControls.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue'
const emit = defineEmits([
'timeJump',
])
const simulatedTimeOffset = defineModel<number>('simulatedTimeOffset', { default: 0 })
const isTimeAccelerated = defineModel<boolean>('isTimeAccelerated', { default: false })
const timeMultiplier = defineModel<number>('timeMultiplier', { default: 60 * 60 * 24 })
const customTimeValue = ref(1)
const customTimeUnit = ref('days')
const timeMultiplierPresets = [
{ label: '1 hour/s', value: 60 * 60 },
{ label: '6 hours/s', value: 60 * 60 * 6 },
{ label: '1 day/s', value: 60 * 60 * 24 },
{ label: '3 days/s', value: 60 * 60 * 24 * 3 },
{ label: '1 week/s', value: 60 * 60 * 24 * 7 },
{ label: '1 month/s', value: 60 * 60 * 24 * 30 },
]
// Format the current time with offset
const currentSimulatedTime = computed(() => {
const now = new Date()
now.setSeconds(now.getSeconds() + simulatedTimeOffset.value)
return now.toLocaleString()
})
// Format time multiplier for display
const formattedTimeMultiplier = computed(() => {
const multiplier = timeMultiplier.value
if (multiplier === 60 * 60)
return '1 hour/s'
if (multiplier === 60 * 60 * 6)
return '6 hours/s'
if (multiplier === 60 * 60 * 24)
return '1 day/s'
if (multiplier === 60 * 60 * 24 * 3)
return '3 days/s'
if (multiplier === 60 * 60 * 24 * 7)
return '1 week/s'
if (multiplier === 60 * 60 * 24 * 30)
return '1 month/s'
if (multiplier < 60)
return `${multiplier} seconds/s`
if (multiplier < 60 * 60)
return `${Math.round(multiplier / 60)} minutes/s`
if (multiplier < 60 * 60 * 24)
return `${Math.round(multiplier / (60 * 60))} hours/s`
if (multiplier < 60 * 60 * 24 * 7)
return `${Math.round(multiplier / (60 * 60 * 24))} days/s`
if (multiplier < 60 * 60 * 24 * 30)
return `${Math.round(multiplier / (60 * 60 * 24 * 7))} weeks/s`
return `${Math.round(multiplier / (60 * 60 * 24 * 30))} months/s`
})
function toggleTimeAcceleration() {
isTimeAccelerated.value = !isTimeAccelerated.value
}
function resetTime() {
simulatedTimeOffset.value = 0
isTimeAccelerated.value = false
}
function setCustomTimeMultiplier() {
let multiplier = customTimeValue.value
switch (customTimeUnit.value) {
case 'seconds':
break
case 'minutes':
multiplier *= 60
break
case 'hours':
multiplier *= 60 * 60
break
case 'days':
multiplier *= 60 * 60 * 24
break
case 'weeks':
multiplier *= 60 * 60 * 24 * 7
break
case 'months':
multiplier *= 60 * 60 * 24 * 30
break
}
timeMultiplier.value = multiplier
}
function jumpAhead(amount, unit) {
emit('timeJump', { amount, unit })
}
</script>
<template>
<div class="rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
<h2 class="flex items-center text-lg font-semibold" gap-4>
<div flex-1>
Time Simulation
</div>
<div class="text-sm font-mono">
{{ currentSimulatedTime }}
</div>
<div class="flex flex-wrap items-center gap-4">
<button
:class="{ 'bg-red-100 dark:bg-red-900': isTimeAccelerated, 'bg-green-100 dark:bg-green-900': !isTimeAccelerated }"
class="rounded-lg px-4 py-2 font-medium transition-colors"
@click="toggleTimeAcceleration"
>
<div v-if="isTimeAccelerated" i-solar:pause-bold />
<div v-else i-solar:play-bold />
</button>
<button
class="rounded-lg bg-neutral-200 px-4 py-2 font-medium dark:bg-neutral-700"
@click="resetTime"
>
<div i-solar:restart-line-duotone />
</button>
</div>
</h2>
<!-- Time jump shortcuts -->
<div class="mt-4 flex flex-wrap gap-2">
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="jumpAhead(1, 'hour')"
>
+1 Hour
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="jumpAhead(1, 'day')"
>
+1 Day
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="jumpAhead(1, 'week')"
>
+1 Week
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="jumpAhead(1, 'month')"
>
+1 Month
</button>
</div>
<h2 class="mt-4 flex items-center text-lg font-semibold">
<div flex-1>
Speed
</div>
<div class="text-sm font-mono">
{{ formattedTimeMultiplier }}
</div>
</h2>
<!-- Time multiplier presets -->
<div class="grid grid-cols-2 mt-4 gap-2 md:grid-cols-6 sm:grid-cols-3">
<button
v-for="preset in timeMultiplierPresets"
:key="preset.value"
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
:class="timeMultiplier === preset.value ? 'bg-blue-200 dark:bg-blue-800' : 'bg-blue-100 dark:bg-blue-900'"
@click="timeMultiplier = preset.value"
>
{{ preset.label }}
</button>
</div>
<!-- Custom time multiplier -->
<div class="mt-4 flex flex-wrap items-center gap-2">
<div class="text-sm font-medium">
Custom:
</div>
<input
v-model.number="customTimeValue"
type="number"
min="1"
class="w-20 rounded-lg border-none bg-white p-2 dark:bg-neutral-700"
>
<select
v-model="customTimeUnit"
class="rounded-lg border-none bg-white p-2 dark:bg-neutral-700"
>
<option value="seconds">
seconds/s
</option>
<option value="minutes">
minutes/s
</option>
<option value="hours">
hours/s
</option>
<option value="days">
days/s
</option>
<option value="weeks">
weeks/s
</option>
<option value="months">
months/s
</option>
</select>
<button
class="rounded-lg bg-blue-100 px-3 py-2 text-sm font-medium dark:bg-blue-900"
@click="setCustomTimeMultiplier"
>
Apply
</button>
</div>
</div>
</template>
@@ -1,607 +0,0 @@
<script setup lang="ts">
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
import { useDark } from '@vueuse/core'
import * as d3 from 'd3'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
const props = defineProps<{
memoryData: EmotionalMemoryItem[]
selectedMemoryId: string
decayRate: number
maxDaysToProject: number
longTermThreshold: number
muscleMemoryThreshold: number
joyBoostFactor: number
joyDecaySteepness: number
aversionSpikeFactor: number
aversionStability: number
}>()
interface DataPoint {
x: number
y: number
label: string
type: string
}
const chartContainer = ref(null)
const tooltip = ref(null)
let resizeObserver = null
const isDark = useDark()
// Render chart when dependencies change
watchEffect(() => {
if (chartContainer.value && props.memoryData.length && props.selectedMemoryId) {
renderChart()
}
})
// Render D3 chart
function renderChart() {
// Clear existing chart
d3.select(chartContainer.value).selectAll('*').remove()
// Find the selected memory
const memory = props.memoryData.find(m => m.id === props.selectedMemoryId)
if (!memory)
return
// Get container dimensions
const containerRect = chartContainer.value.getBoundingClientRect()
const containerWidth = containerRect.width
const containerHeight = containerRect.height || 700
// Set chart margins
const margin = { top: 40, right: 30, bottom: 40, left: 60 }
const width = containerWidth - margin.left - margin.right
const height = containerHeight - margin.top - margin.bottom
// Prepare data for chart
const originalScore = Number.parseFloat(String(memory.score))
const joyScore = Number.parseFloat(String(memory.joy_score))
const aversionScore = Number.parseFloat(String(memory.aversion_score))
const retrievalCount = Number.parseInt(String(memory.retrieval_count))
const dr = props.decayRate
const maxDays = props.maxDaysToProject
const ageInDays = Number.parseFloat(String(memory.age_in_seconds)) / (24 * 60 * 60)
const timeSinceRetrievalDays = Number.parseFloat(String(memory.time_since_retrieval)) / (24 * 60 * 60)
// Calculate LTM factor
const ltmFactor = retrievalCount >= props.longTermThreshold
? 1.0 - (0.3 ** (retrievalCount / props.longTermThreshold))
: 0
// Calculate current score components
const currentJoyEffect = joyScore * props.joyBoostFactor
* Math.exp(-props.joyDecaySteepness * timeSinceRetrievalDays)
const currentAversionEffect = aversionScore * props.aversionSpikeFactor
* props.aversionStability ** Math.ceil(retrievalCount / 5)
// Generate baseline data (standard decay without emotional effect)
const baselineData = [{
x: 0,
y: originalScore,
label: 'Original',
type: 'baseline',
}]
// Generate data with emotional effects
const emotionalData = [{
x: 0,
y: originalScore,
label: 'Original',
type: 'emotional',
}]
// Generate joy component data
const joyData = [{
x: 0,
y: joyScore * props.joyBoostFactor * originalScore,
label: 'Original Joy',
type: 'joy',
}]
// Generate aversion component data
const aversionData = [{
x: 0,
y: aversionScore * props.aversionSpikeFactor * originalScore,
label: 'Original Aversion',
type: 'aversion',
}]
// Current point (today)
baselineData.push({
x: ageInDays,
y: originalScore * Math.exp(-dr * ageInDays * (1 - ltmFactor)),
label: 'Current',
type: 'baseline',
})
emotionalData.push({
x: ageInDays,
y: Number.parseFloat(String(memory.decayed_score)),
label: 'Current',
type: 'emotional',
})
joyData.push({
x: ageInDays,
y: originalScore * currentJoyEffect,
label: 'Current Joy',
type: 'joy',
})
aversionData.push({
x: ageInDays,
y: originalScore * currentAversionEffect,
label: 'Current Aversion',
type: 'aversion',
})
// Add future projection points
const dayStep = Math.ceil(maxDays / 20) // Adjust steps based on projection days
for (let day = dayStep; day <= maxDays; day += dayStep) {
const projectedDay = ageInDays + day
const projectedDaysSinceRetrieval = timeSinceRetrievalDays + day
const label = `+${day}d`
// Calculate LTM-adjusted decay
const baseDecay = Math.exp(-dr * projectedDay * (1 - ltmFactor))
// Joy decays more quickly over time
const joyEffect = joyScore * props.joyBoostFactor
* Math.exp(-props.joyDecaySteepness * projectedDaysSinceRetrieval)
// Aversion is more stable, especially with repeated retrievals
const aversionEffect = aversionScore * props.aversionSpikeFactor
* props.aversionStability ** Math.ceil(retrievalCount / 5)
// Combined emotional effect
const emotionalEffect = (1 + joyEffect) * (1 + aversionEffect)
// Add baseline point (standard decay)
baselineData.push({
x: projectedDay,
y: originalScore * baseDecay,
label,
type: 'baseline',
})
// Add emotional point (with joy and aversion effects)
emotionalData.push({
x: projectedDay,
y: originalScore * baseDecay * emotionalEffect,
label,
type: 'emotional',
})
// Add joy component (isolates joy effect)
joyData.push({
x: projectedDay,
y: originalScore * joyEffect,
label,
type: 'joy',
})
// Add aversion component (isolates aversion effect)
aversionData.push({
x: projectedDay,
y: originalScore * aversionEffect,
label,
type: 'aversion',
})
}
// Create tooltip if it doesn't exist
if (!tooltip.value) {
tooltip.value = d3.select(chartContainer.value)
.append('div')
.attr('class', 'absolute pointer-events-none transition-opacity duration-200 bg-white dark:bg-neutral-800 p-2 rounded shadow-md text-sm z-10')
.style('opacity', 0)
}
// Create SVG
const svg = d3.select(chartContainer.value)
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
// Set up scales
const xExtent = [0, maxDays + ageInDays]
const yExtent = [0, d3.max([
...emotionalData,
...baselineData,
...joyData,
...aversionData,
], d => d.y) * 1.1]
const xScale = d3.scaleLinear()
.domain(xExtent)
.range([0, width])
const yScale = d3.scaleLinear()
.domain(yExtent)
.range([height, 0])
// Add axes and gridlines
addAxesAndGrids(svg, xScale, yScale, width, height)
// Add data lines
addDataLines(svg, baselineData, emotionalData, joyData, aversionData, xScale, yScale)
// Add key threshold markers
if (retrievalCount > 0) {
const halfLife = Math.log(2) / dr
if (halfLife <= maxDays + ageInDays) {
addThresholdLine(svg, halfLife, xScale, height, 'Half-life', 'rgba(220, 38, 38, 0.6)')
}
}
// Add memory type marker
let memoryTypeLabel = 'Short-term Memory'
let memoryTypeColor = 'rgba(220, 38, 38, 0.8)'
if (retrievalCount >= props.muscleMemoryThreshold) {
memoryTypeLabel = 'Muscle Memory'
memoryTypeColor = 'rgba(139, 92, 246, 0.8)'
}
else if (retrievalCount >= props.longTermThreshold) {
memoryTypeLabel = 'Long-term Memory'
memoryTypeColor = 'rgba(59, 130, 246, 0.8)'
}
else if (retrievalCount > 0) {
memoryTypeLabel = 'Working Memory'
memoryTypeColor = 'rgba(14, 165, 233, 0.8)'
}
addMemoryTypeMarker(svg, memoryTypeLabel, memoryTypeColor, retrievalCount, width)
// Add chart title and legends
addTitleAndLegends(svg, memory.id, joyScore, aversionScore, retrievalCount, width)
// Add data points with interaction
addDataPoints(svg, emotionalData, baselineData, xScale, yScale)
}
function addAxesAndGrids(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
width: number,
height: number,
): void {
const xAxis = d3.axisBottom(xScale)
.ticks(5)
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())} days`)
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())}`)
// Add gridlines
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(() => ''),
)
.selectAll('line')
.attr('stroke', isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.7)')
.attr('stroke-width', 1)
svg.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(() => ''),
)
.selectAll('line')
.attr('stroke', isDark.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.7)')
.attr('stroke-width', 1)
// Add axes
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.append('text')
.attr('class', 'axis-label')
.attr('x', width / 2)
.attr('y', 36)
.attr('text-anchor', 'middle')
.text('Time (days)')
.attr('fill', 'currentColor')
svg.append('g')
.attr('class', 'y-axis')
.call(yAxis)
.append('text')
.attr('class', 'axis-label')
.attr('transform', 'rotate(-90)')
.attr('y', -40)
.attr('x', -height / 2)
.attr('text-anchor', 'middle')
.text('Memory Strength')
.attr('fill', 'currentColor')
}
function addDataLines(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
baselineData: DataPoint[],
emotionalData: DataPoint[],
joyData: DataPoint[],
aversionData: DataPoint[],
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
): void {
// Create line generator
const line = d3.line<DataPoint>()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX)
// Add baseline decay line
svg.append('path')
.datum(baselineData)
.attr('class', 'line baseline')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(156, 163, 175, 0.8)')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,3')
// Add joy component line (transparent fill under curve)
svg.append('path')
.datum(joyData)
.attr('class', 'line joy')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(250, 204, 21, 0.8)')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '3,2')
// Add aversion component line (transparent fill under curve)
svg.append('path')
.datum(aversionData)
.attr('class', 'line aversion')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(220, 38, 38, 0.8)')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '3,2')
// Add emotional effect line (main result)
svg.append('path')
.datum(emotionalData)
.attr('class', 'line emotional')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(59, 130, 246, 1)')
.attr('stroke-width', 3)
}
function addThresholdLine(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
position: number,
xScale: d3.ScaleLinear<number, number>,
height: number,
label: string,
color: string,
): void {
svg.append('line')
.attr('class', 'threshold-line')
.attr('x1', xScale(position))
.attr('y1', 0)
.attr('x2', xScale(position))
.attr('y2', height)
.attr('stroke', color)
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5')
svg.append('text')
.attr('class', 'threshold-label')
.attr('x', xScale(position))
.attr('y', -8)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('fill', color)
.text(`${label}: ${position.toFixed(1)} days`)
}
function addMemoryTypeMarker(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
typeLabel: string,
color: string,
retrievalCount: number,
width: number,
): void {
svg.append('text')
.attr('class', 'memory-type-label')
.attr('x', width / 2)
.attr('y', -20)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.attr('fill', color)
.text(`${typeLabel} (${retrievalCount} retrievals)`)
}
function addDataPoints(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
emotionalData: DataPoint[],
baselineData: DataPoint[],
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
): void {
// Add key data points for emotional line
svg.selectAll('.point-emotional')
.data(emotionalData.filter((d, i) => i === 0 || i === 1 || i % 5 === 0))
.enter()
.append('circle')
.attr('class', 'point-emotional')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', (d, i) => i < 2 ? 6 : 4)
.attr('fill', 'rgba(59, 130, 246, 0.8)')
.attr('stroke', 'white')
.attr('stroke-width', 2)
.on('mouseover', function (event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
const baselinePoint = baselineData.find(bd => bd.x === d.x)
const baselineValue = baselinePoint ? Math.round(baselinePoint.y) : 'N/A'
tooltip.value
.style('opacity', 1)
.html(`
<div class="font-semibold">${d.label}</div>
<div>Total: ${Math.round(d.y)}</div>
<div>Base: ${baselineValue}</div>
<div>Day: ${Math.round(d.x)}</div>
`)
.style('left', `${event.offsetX + 15}px`)
.style('top', `${event.offsetY - 28}px`)
})
.on('mouseout', function () {
d3.select(this)
.transition()
.duration(200)
.attr('r', (d, i) => i < 2 ? 6 : 4)
tooltip.value
.transition()
.duration(200)
.style('opacity', 0)
})
// Add labels to important emotional points
svg.selectAll('.emotional-point-label')
.data(emotionalData.filter((d, i) => i === 0 || i === 1 || i === emotionalData.length - 1))
.enter()
.append('text')
.attr('class', 'emotional-point-label')
.attr('x', d => xScale(d.x))
.attr('y', d => yScale(d.y) - 15)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('fill', 'rgba(59, 130, 246, 1)')
.text(d => Math.round(d.y))
}
function addTitleAndLegends(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
memoryId: string,
joyScore: number,
aversionScore: number,
retrievalCount: number,
width: number,
): void {
// Add emotional scores label
const joyPercent = Math.round(joyScore * 100)
const aversionPercent = Math.round(aversionScore * 100)
// Add legend
const legendData = [
{ label: 'Emotional Memory', color: 'rgba(59, 130, 246, 0.8)' },
{ label: 'Base Decay', color: 'rgba(156, 163, 175, 0.8)' },
{ label: `Joy Component (${joyPercent}%)`, color: 'rgba(250, 204, 21, 0.8)' },
{ label: `Aversion Component (${aversionPercent}%)`, color: 'rgba(220, 38, 38, 0.8)' },
]
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 220}, 10)`)
const legendItems = legend.selectAll('.legend-item')
.data(legendData)
.enter()
.append('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(10, ${i * 20})`)
legendItems.append('line')
.attr('x1', 0)
.attr('y1', 8)
.attr('x2', 20)
.attr('y2', 8)
.attr('stroke', d => d.color)
.attr('stroke-width', 2)
.attr('stroke-dasharray', (d, i) => i === 0 ? '0' : i === 1 ? '5,3' : '3,2')
legendItems.append('text')
.attr('x', 25)
.attr('y', 12)
.attr('font-size', '12px')
.attr('fill', 'currentColor')
.text(d => d.label)
}
onMounted(() => {
// Setup resize observer
resizeObserver = new ResizeObserver(() => {
if (props.memoryData.length && props.selectedMemoryId) {
renderChart()
}
})
if (chartContainer.value) {
resizeObserver.observe(chartContainer.value)
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
})
</script>
<template>
<div class="rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<h2 class="mb-4 text-lg font-semibold">
Emotional Memory Projection
</h2>
<!-- D3.js chart container -->
<div ref="chartContainer" class="relative h-[800px] w-full">
<!-- D3 will render the chart here -->
</div>
</div>
</template>
<style scoped>
/* D3 Chart Styles */
:deep(.axis-label) {
font-size: 12px;
}
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #dce0e3;
}
.dark {
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #374151;
}
}
</style>
@@ -1,339 +0,0 @@
<script setup lang="ts">
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
import { computed } from 'vue'
const props = defineProps<{
memory: EmotionalMemoryItem
longTermThreshold: number
muscleMemoryThreshold: number
}>()
const emit = defineEmits(['retrieve'])
// Calculate age in days
const ageInDays = computed(() => {
return Math.round(props.memory.age_in_seconds / (24 * 60 * 60))
})
// Calculate time since last retrieval
const daysSinceRetrieved = computed(() => {
return Math.round(props.memory.time_since_retrieval / (24 * 60 * 60))
})
// Calculate memory status
const memoryStatus = computed(() => {
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
return {
type: 'muscle-memory',
label: 'Muscle Memory',
color: 'text-purple-600 dark:text-purple-400',
}
}
else if (props.memory.retrieval_count >= props.longTermThreshold) {
return {
type: 'long-term',
label: 'Long-term Memory',
color: 'text-indigo-600 dark:text-indigo-400',
}
}
else if (props.memory.retrieval_count > 0) {
return {
type: 'working',
label: 'Working Memory',
color: 'text-blue-600 dark:text-blue-400',
}
}
else {
return {
type: 'short-term',
label: 'Short-term Memory',
color: 'text-red-600 dark:text-red-400',
}
}
})
// Joy and aversion levels as percentage
const joyPercentage = computed(() => {
return Math.round(props.memory.joy_score * 100)
})
const aversionPercentage = computed(() => {
return Math.round(props.memory.aversion_score * 100)
})
// Progress percentage for memory strength
const strengthPercentage = computed(() => {
return Math.min(100, Math.round((props.memory.decayed_score / props.memory.score) * 100))
})
// Progress towards long-term memory
const ltmPercentage = computed(() => {
if (props.memory.retrieval_count >= props.longTermThreshold) {
return 100
}
return Math.round((props.memory.retrieval_count / props.longTermThreshold) * 100)
})
// Progress towards muscle memory
const musclePercentage = computed(() => {
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
return 100
}
if (props.memory.retrieval_count < props.longTermThreshold) {
return 0
}
return Math.round(((props.memory.retrieval_count - props.longTermThreshold)
/ (props.muscleMemoryThreshold - props.longTermThreshold)) * 100)
})
// Emotional effect on memory score
const emotionalMultiplier = computed(() => {
// This is a simplified calculation - would match your SQL formula
const joyEffect = props.memory.joy_score > 0 ? props.memory.joy_score : 0
const aversionEffect = props.memory.aversion_score > 0 ? props.memory.aversion_score : 0
const combined = 1 + joyEffect + aversionEffect
return Math.round(combined * 100)
})
function simulateRetrieval(emotionalResponse = null) {
if (emotionalResponse === 'joy') {
emit('retrieve', props.memory.id, { joyModifier: 0.1, aversionModifier: -0.05 })
}
else if (emotionalResponse === 'aversion') {
emit('retrieve', props.memory.id, { joyModifier: -0.05, aversionModifier: 0.1 })
}
else if (emotionalResponse === 'neutral') {
emit('retrieve', props.memory.id, { joyModifier: 0, aversionModifier: 0 })
}
else if (emotionalResponse === 'strong-joy') {
emit('retrieve', props.memory.id, { joyModifier: 0.2, aversionModifier: -0.1 })
}
else if (emotionalResponse === 'strong-aversion') {
emit('retrieve', props.memory.id, { joyModifier: -0.1, aversionModifier: 0.2 })
}
}
</script>
<template>
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<div class="mb-4 flex justify-between">
<h3 class="text-xl font-bold">
{{ memory.id }}
</h3>
<span
class="inline-flex items-center rounded-lg px-2 py-1 text-xs font-medium"
:class="{
'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300': memoryStatus.type === 'muscle-memory',
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300': memoryStatus.type === 'long-term',
'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300': memoryStatus.type === 'working',
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300': memoryStatus.type === 'short-term',
}"
>
{{ memoryStatus.label }}
</span>
</div>
<div class="grid grid-cols-2 mb-4 gap-3 text-sm font-mono">
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Base Score:
</div>
<div class="text-right font-medium">
{{ Math.round(memory.score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Current Score:
</div>
<div class="text-right font-medium">
{{ Math.round(memory.decayed_score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Emotional Effect:
</div>
<div class="text-right font-medium">
{{ emotionalMultiplier }}%
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Retrieval Count:
</div>
<div class="text-right font-medium">
{{ memory.retrieval_count }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Memory Age:
</div>
<div class="text-right font-medium">
{{ ageInDays }} days
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Last Retrieved:
</div>
<div class="text-right font-medium">
{{ daysSinceRetrieved > 0 ? `${daysSinceRetrieved} days ago` : 'Today' }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Joy Score:
</div>
<div
class="text-right font-medium"
:class="joyPercentage > 50 ? 'text-yellow-500 dark:text-yellow-400' : ''"
>
{{ joyPercentage }}%
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Aversion Score:
</div>
<div
class="text-right font-medium"
:class="aversionPercentage > 50 ? 'text-red-500 dark:text-red-400' : ''"
>
{{ aversionPercentage }}%
</div>
</div>
<!-- Emotional retrieval buttons -->
<div class="mb-6">
<h4 class="mb-2 text-sm text-neutral-500 font-medium dark:text-neutral-400">
Simulate Retrieval with Emotion:
</h4>
<div class="grid grid-cols-3 gap-2">
<button
class="rounded-lg bg-yellow-100 px-3 py-2 text-sm font-medium dark:bg-yellow-900/50 hover:bg-yellow-200 dark:hover:bg-yellow-900"
@click="simulateRetrieval('strong-joy')"
>
Very Joyful
</button>
<button
class="rounded-lg bg-yellow-50 px-3 py-2 text-sm font-medium dark:bg-yellow-900/20 hover:bg-yellow-100 dark:hover:bg-yellow-900/40"
@click="simulateRetrieval('joy')"
>
Mild Joy
</button>
<button
class="rounded-lg bg-neutral-100 px-3 py-2 text-sm font-medium dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700"
@click="simulateRetrieval('neutral')"
>
Neutral
</button>
<button
class="rounded-lg bg-red-50 px-3 py-2 text-sm font-medium dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40"
@click="simulateRetrieval('aversion')"
>
Mild Aversion
</button>
<button
class="rounded-lg bg-red-100 px-3 py-2 text-sm font-medium dark:bg-red-900/50 hover:bg-red-200 dark:hover:bg-red-900"
@click="simulateRetrieval('strong-aversion')"
>
Strong Aversion
</button>
</div>
</div>
<!-- Visual representation of memory state -->
<div>
<!-- Memory Strength Progress -->
<div class="mt-4 font-mono">
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Current Memory Strength:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full transition-all duration-500"
:class="{
'bg-purple-400': memoryStatus.type === 'muscle-memory',
'bg-indigo-500': memoryStatus.type === 'long-term',
'bg-blue-500': memoryStatus.type === 'working',
'bg-red-500': memoryStatus.type === 'short-term',
}"
:style="`width: ${strengthPercentage}%`"
/>
</div>
<div class="mt-1 flex justify-between text-xs">
<span>0%</span>
<span>{{ Math.round(memory.score / 2) }}</span>
<span>{{ Math.round(memory.score) }}</span>
</div>
</div>
<!-- Memory Formation Progress -->
<div class="mt-4 font-mono">
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Long-term Memory Progress:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-indigo-400 transition-all duration-500"
:style="`width: ${ltmPercentage}%`"
/>
</div>
<div class="mt-1 flex items-center justify-between text-xs">
<span>Working</span>
<span v-if="memory.retrieval_count < longTermThreshold" class="text-sm font-medium">
{{ memory.retrieval_count }}/{{ longTermThreshold }} retrievals
</span>
<span v-else class="text-sm text-indigo-600 font-medium dark:text-indigo-400">
LTM Stabled
</span>
<span>Long-term</span>
</div>
</div>
<!-- Muscle Memory Progress -->
<div class="mt-4 font-mono">
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Muscle Memory Progress:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-purple-400 transition-all duration-500"
:style="`width: ${musclePercentage}%`"
/>
</div>
<div class="mt-1 flex items-center justify-between text-xs">
<span>Conscious</span>
<span v-if="memory.retrieval_count < muscleMemoryThreshold" class="text-sm font-medium">
{{ memory.retrieval_count }}/{{ muscleMemoryThreshold }} retrievals
</span>
<span v-else class="text-sm text-purple-600 font-medium dark:text-purple-400">
MM formed
</span>
<span>Automatic</span>
</div>
</div>
<!-- Emotional Components -->
<div class="grid grid-cols-2 mt-4 gap-4">
<div>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Joy Factor:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-yellow-400 transition-all duration-500"
:style="`width: ${joyPercentage}%`"
/>
</div>
</div>
<div>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Aversion Factor:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-red-400 transition-all duration-500"
:style="`width: ${aversionPercentage}%`"
/>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1,269 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
memory: any
longTermMemoryThreshold: number
muscleMemoryThreshold: number
}>()
const emit = defineEmits(['retrieve'])
// Joy and aversion values
const joyLevel = computed(() => {
return props.memory.joy_score || 0
})
const aversionLevel = computed(() => {
return props.memory.aversion_score || 0
})
// Calculate age in days
const ageInDays = computed(() => {
return Math.round(props.memory.age_in_seconds / (24 * 60 * 60))
})
// Calculate memory status
const memoryStatus = computed(() => {
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
return {
type: 'muscle-memory',
label: 'Muscle Memory',
color: 'text-purple-600 dark:text-purple-400',
}
}
else if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
return {
type: 'long-term',
label: 'Long-term',
color: 'text-indigo-600 dark:text-indigo-400',
}
}
else if (props.memory.retrieval_count > 0) {
return {
type: 'working',
label: 'Working',
color: 'text-blue-600 dark:text-blue-400',
}
}
else {
return {
type: 'short-term',
label: 'Short-term',
color: 'text-red-600 dark:text-red-400',
}
}
})
// Progress percentage for memory strength
const strengthPercentage = computed(() => {
return Math.round((props.memory.decayed_score / props.memory.score) * 100)
})
// Progress towards long-term memory
const ltmPercentage = computed(() => {
if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
return 100
}
return Math.round((props.memory.retrieval_count / props.longTermMemoryThreshold) * 100)
})
// Progress towards muscle memory
const musclePercentage = computed(() => {
if (props.memory.retrieval_count >= props.muscleMemoryThreshold) {
return 100
}
return Math.round((props.memory.retrieval_count / props.muscleMemoryThreshold) * 100)
})
function simulateRetrieval(emotionalResponse = null) {
if (emotionalResponse === 'joy') {
emit('retrieve', props.memory.id, { joyModifier: 0.1, aversionModifier: -0.05 })
}
else if (emotionalResponse === 'aversion') {
emit('retrieve', props.memory.id, { joyModifier: -0.05, aversionModifier: 0.1 })
}
else {
emit('retrieve', props.memory.id, { joyModifier: 0, aversionModifier: 0 })
}
}
</script>
<template>
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<div class="mb-4 flex justify-between">
<h3 class="font-bold">
{{ memory.id }}
</h3>
<div class="flex gap-2">
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-xs font-medium dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800" h-fit
@click="simulateRetrieval()"
>
Neutral Retrieval
</button>
</div>
</div>
<div class="grid grid-cols-2 mb-4 gap-3" text-sm font-mono>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Memory Phase:
</div>
<div class="font-medium" text-right text-nowrap :class="memoryStatus.color">
{{ memoryStatus.label }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Base Score:
</div>
<div class="font-medium" text-right>
{{ Math.round(memory.score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Current Score:
</div>
<div class="font-medium" text-right>
{{ Math.round(memory.decayed_score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Retrieval Count:
</div>
<div class="font-medium" text-right>
{{ memory.retrieval_count }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Age:
</div>
<div class="font-medium" text-right>
{{ ageInDays }} days
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Joy Score:
</div>
<div class="font-medium" text-right :class="joyLevel > 0.5 ? 'text-yellow-500' : ''">
{{ (joyLevel * 100).toFixed(0) }}%
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Aversion Score:
</div>
<div class="font-medium" text-right :class="aversionLevel > 0.5 ? 'text-red-500' : ''">
{{ (aversionLevel * 100).toFixed(0) }}%
</div>
</div>
<!-- Emotional response buttons -->
<div class="grid grid-cols-2 mb-4 gap-2">
<button
class="rounded-lg bg-yellow-100 px-3 py-2 text-sm font-medium dark:bg-yellow-900 hover:bg-yellow-200 dark:hover:bg-yellow-800"
@click="simulateRetrieval('joy')"
>
Joyful Retrieval (+0.1)
</button>
<button
class="rounded-lg bg-red-100 px-3 py-2 text-sm font-medium dark:bg-red-900 hover:bg-red-200 dark:hover:bg-red-800"
@click="simulateRetrieval('aversion')"
>
Aversive Retrieval (+0.1)
</button>
</div>
<!-- Visual representation of current state -->
<div>
<!-- Memory Strength Progress -->
<div class="mt-4" font-mono>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Memory Strength:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full transition-all duration-500"
:class="memoryStatus.type === 'muscle-memory' ? 'bg-purple-400' : memoryStatus.type === 'long-term' ? 'bg-indigo-500' : 'bg-blue-500'"
:style="`width: ${strengthPercentage}%`"
/>
</div>
<div class="mt-1 flex justify-between text-xs">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
</div>
<!-- Long-term Memory Progress -->
<div class="mt-4" font-mono>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Long-term Memory Progress:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-indigo-400 transition-all duration-500"
:style="`width: ${ltmPercentage}%`"
/>
</div>
<div class="mt-1 flex items-center justify-between text-xs">
<span>Short-term</span>
<span v-if="memory.retrieval_count < longTermMemoryThreshold" class="text-sm font-medium">
{{ memory.retrieval_count }}/{{ longTermMemoryThreshold }} retrievals
</span>
<span v-else class="text-sm text-indigo-600 font-medium dark:text-indigo-400">
LTM formed
</span>
<span>Long-term</span>
</div>
</div>
<!-- Muscle Memory Progress -->
<div class="mt-4" font-mono>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Muscle Memory Progress:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-purple-400 transition-all duration-500"
:style="`width: ${musclePercentage}%`"
/>
</div>
<div class="mt-1 flex items-center justify-between text-xs">
<span>Conscious</span>
<span v-if="memory.retrieval_count < muscleMemoryThreshold" class="text-sm font-medium">
{{ memory.retrieval_count }}/{{ muscleMemoryThreshold }} retrievals
</span>
<span v-else class="text-sm text-purple-600 font-medium dark:text-purple-400">
MM formed
</span>
<span>Automatic</span>
</div>
</div>
<!-- Emotional Components -->
<div class="grid grid-cols-2 mt-4 gap-4">
<div>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Joy Factor:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-yellow-400 transition-all duration-500"
:style="`width: ${joyLevel * 100}%`"
/>
</div>
</div>
<div>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Aversion Factor:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full bg-red-400 transition-all duration-500"
:style="`width: ${aversionLevel * 100}%`"
/>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1,132 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import Range from '../Range.vue'
const joyBoostFactor = defineModel<number>('joyBoostFactor', { default: 1.5 })
const joyDecaySteepness = defineModel<number>('joyDecaySteepness', { default: 3.0 })
const aversionSpikeFactor = defineModel<number>('aversionSpikeFactor', { default: 2.0 })
const aversionStability = defineModel<number>('aversionStability', { default: 1.2 })
const randomRecallProbability = defineModel<number>('randomRecallProbability', { default: 0.05 })
const flashbackIntensity = defineModel<number>('flashbackIntensity', { default: 2.0 })
// Computed percent values for display
const randomRecallPercent = computed(() => (randomRecallProbability.value * 100).toFixed(0))
</script>
<template>
<div>
<div class="my-4 rounded-lg bg-neutral-100 dark:bg-neutral-800/50">
<h2 class="mb-2 text-lg font-semibold">
Emotional Memory Parameters
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Joy/Euphoria Settings -->
<div class="flex flex-col gap-2">
<label class="font-medium">Joy Boost Factor</label>
<div class="flex items-center gap-2">
<Range
v-model="joyBoostFactor"
:min="0.1"
:max="3.0"
:step="0.1"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ joyBoostFactor.toFixed(1) }}x</span>
</div>
<div class="text-xs text-neutral-500">
How much joy increases memory strength
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Joy Decay Steepness</label>
<div class="flex items-center gap-2">
<Range
v-model="joyDecaySteepness"
:min="0.5"
:max="5.0"
:step="0.1"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ joyDecaySteepness.toFixed(1) }}</span>
</div>
<div class="text-xs text-neutral-500">
How quickly joy effect fades (higher = faster)
</div>
</div>
<!-- Aversion/Trauma Settings -->
<div class="flex flex-col gap-2">
<label class="font-medium">Aversion Spike Factor</label>
<div class="flex items-center gap-2">
<Range
v-model="aversionSpikeFactor"
:min="0.1"
:max="5.0"
:step="0.1"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ aversionSpikeFactor.toFixed(1) }}x</span>
</div>
<div class="text-xs text-neutral-500">
How strongly aversive memories spike in recall
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Aversion Stability</label>
<div class="flex items-center gap-2">
<Range
v-model="aversionStability"
:min="1.0"
:max="1.5"
:step="0.05"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ aversionStability.toFixed(2) }}</span>
</div>
<div class="text-xs text-neutral-500">
How persistent aversive memories become (PTSD-like)
</div>
</div>
<!-- Random Recall/Flashback -->
<div class="flex flex-col gap-2">
<label class="font-medium">Random Recall Probability</label>
<div class="flex items-center gap-2">
<Range
v-model="randomRecallProbability"
:min="0"
:max="0.3"
:step="0.01"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ randomRecallPercent }}%</span>
</div>
<div class="text-xs text-neutral-500">
Chance of random memory flashbacks
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium">Flashback Intensity</label>
<div class="flex items-center gap-2">
<Range
v-model="flashbackIntensity"
:min="1.0"
:max="5.0"
:step="0.1"
class="w-full"
/>
<span class="w-16 text-right font-mono">{{ flashbackIntensity.toFixed(1) }}x</span>
</div>
<div class="text-xs text-neutral-500">
How strong random memory flashbacks can be
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1,487 +0,0 @@
<script setup lang="ts">
import type { EmotionalMemoryItem } from '../../types/memory/emotional-memory'
import * as d3 from 'd3'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
const props = defineProps<{
memoryData: EmotionalMemoryItem[]
simulatedTimeOffset: number
timeRange: number // days to display in the heatmap
}>()
interface HeatmapCell {
id: string
day: number
value: number
isRetrieval: boolean
isRandom: boolean
joyScore: number
aversionScore: number
}
const heatmapContainer = ref<HTMLDivElement>()
const tooltip = ref<d3.Selection<HTMLDivElement, unknown, null, undefined>>()
let resizeObserver: ResizeObserver | undefined
// Add mode toggle
const viewMode = ref<'patterns' | 'counts'>('patterns')
watchEffect(() => {
if (heatmapContainer.value && props.memoryData.length > 0) {
viewMode.value === 'patterns' ? renderHeatmap() : renderCountHeatmap()
}
})
function renderHeatmap() {
// Clear existing chart
d3.select(heatmapContainer.value).selectAll('*').remove()
// Get container dimensions
const containerRect = heatmapContainer.value.getBoundingClientRect()
const containerWidth = containerRect.width
const containerHeight = 300 // Fixed height for the heatmap
// Set chart margins
const margin = { top: 30, right: 20, bottom: 60, left: 120 }
const width = containerWidth - margin.left - margin.right
const height = containerHeight - margin.top - margin.bottom
// Prepare data for heatmap
const heatmapData: HeatmapCell[] = []
// Generate heatmap data from memory items
for (const memory of props.memoryData) {
// const ageInDays = Math.floor(memory.age_in_seconds / (24 * 60 * 60))
const retrievalDaysAgo = Math.floor(memory.time_since_retrieval / (24 * 60 * 60))
// Only consider memories retrieved within the time range
if (retrievalDaysAgo <= props.timeRange) {
// For each memory, add a heatmap cell for the retrieval day
heatmapData.push({
id: memory.id,
day: props.timeRange - retrievalDaysAgo,
value: 1, // Strength of retrieval
isRetrieval: true,
isRandom: Math.random() < 0.2, // Simulate random recall events (20% chance)
joyScore: memory.joy_score,
aversionScore: memory.aversion_score,
})
// Add additional cells for fade trail of past retrievals
const fadeLength = 5 // Days of fade trail
for (let i = 1; i <= fadeLength; i++) {
if (props.timeRange - retrievalDaysAgo - i >= 0) {
heatmapData.push({
id: memory.id,
day: props.timeRange - retrievalDaysAgo - i,
value: 0.2 * (fadeLength - i + 1) / fadeLength, // Decreasing intensity
isRetrieval: false,
isRandom: false,
joyScore: memory.joy_score * 0.5, // Reduced emotional impact
aversionScore: memory.aversion_score * 0.5,
})
}
}
}
}
// Create tooltip if it doesn't exist
if (!tooltip.value) {
tooltip.value = d3.select(heatmapContainer.value)
.append('div')
.attr('class', 'absolute pointer-events-none transition-opacity duration-200 bg-white dark:bg-neutral-800 p-2 rounded shadow-md text-sm z-10')
.style('opacity', 0)
}
// Create SVG
const svg = d3.select(heatmapContainer.value)
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
// Extract unique memory IDs
const memoryIds = Array.from(new Set(props.memoryData.map(m => m.id)))
// Set up scales
const xScale = d3.scaleLinear()
.domain([0, props.timeRange])
.range([0, width])
const yScale = d3.scaleBand()
.domain(memoryIds)
.range([0, height])
.padding(0.1)
const colorScale = d3.scaleSequential()
.domain([0, 1])
.interpolator(d3.interpolateBlues)
// Create x-axis (time)
const xAxis = d3.axisBottom(xScale)
.ticks(Math.min(props.timeRange, 10))
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.selectAll('text')
.attr('transform', 'rotate(-45)')
.style('text-anchor', 'end')
// Create y-axis (memory IDs)
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.attr('class', 'y-axis')
.call(yAxis)
// Create heatmap cells
svg.selectAll('.heatmap-cell')
.data(heatmapData)
.enter()
.append('rect')
.attr('class', 'heatmap-cell')
.attr('x', d => xScale(d.day))
.attr('y', d => yScale(d.id))
.attr('width', _d => Math.max(2, width / props.timeRange)) // Ensure cells are visible
.attr('height', yScale.bandwidth())
.attr('fill', (d) => {
if (d.isRetrieval) {
// Create color based on emotional components
if (d.joyScore > 0.5)
return d3.interpolateYlOrRd(d.joyScore)
if (d.aversionScore > 0.5)
return d3.interpolatePurples(d.aversionScore)
return colorScale(d.value)
}
else {
// Fade trails
return colorScale(d.value)
}
})
.attr('stroke', d => d.isRandom ? 'rgba(255, 0, 0, 0.5)' : 'none')
.attr('stroke-width', 1)
.on('mouseover', function (event, d) {
d3.select(this)
.attr('stroke', '#fff')
.attr('stroke-width', 2)
let tooltipContent = `
<div class="font-semibold">${d.id}</div>
<div>${d.isRetrieval ? 'Retrieval' : 'Echo'}</div>
<div>${props.timeRange - d.day} days ago</div>
`
if (d.isRetrieval) {
tooltipContent += `
<div>Joy: ${Math.round(d.joyScore * 100)}%</div>
<div>Aversion: ${Math.round(d.aversionScore * 100)}%</div>
${d.isRandom ? '<div class="text-red-500">Random Recall</div>' : ''}
`
}
tooltip.value
.style('opacity', 1)
.html(tooltipContent)
.style('left', `${event.offsetX + 10}px`)
.style('top', `${event.offsetY - 15}px`)
})
.on('mouseout', function () {
d3.select(this)
// @ts-expect-error - d is of type HeatmapCell
.attr('stroke', d => d.isRandom ? 'rgba(255, 0, 0, 0.5)' : 'none')
// @ts-expect-error - d is of type HeatmapCell
.attr('stroke-width', d => d.isRandom ? 1 : 0)
tooltip.value
.style('opacity', 0)
})
// Add legend
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 120}, -30)`)
// Legend items
const legendItems = [
{ label: 'Retrieval', color: colorScale(1) },
{ label: 'Joy', color: d3.interpolateYlOrRd(0.8) },
{ label: 'Aversion', color: d3.interpolatePurples(0.8) },
{ label: 'Random', color: 'rgba(70, 130, 180, 0.8)', stroke: 'rgba(255, 0, 0, 0.5)' },
]
legendItems.forEach((item, i) => {
const g = legend.append('g')
.attr('transform', `translate(${i * 70}, 0)`)
g.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', item.color)
.attr('stroke', item.stroke || 'none')
.attr('stroke-width', item.stroke ? 1 : 0)
g.append('text')
.attr('x', 20)
.attr('y', 12)
.attr('font-size', '10px')
.attr('fill', 'currentColor')
.text(item.label)
})
}
// New function to render the count heatmap
function renderCountHeatmap() {
// Clear existing chart
d3.select(heatmapContainer.value).selectAll('*').remove()
// Get container dimensions
const containerRect = heatmapContainer.value.getBoundingClientRect()
const containerWidth = containerRect.width
const containerHeight = 300 // Fixed height for the heatmap
// Set chart margins
const margin = { top: 30, right: 20, bottom: 60, left: 120 }
const width = containerWidth - margin.left - margin.right
const height = containerHeight - margin.top - margin.bottom
// Create tooltip if it doesn't exist
if (!tooltip.value) {
tooltip.value = d3.select(heatmapContainer.value)
.append('div')
.attr('class', 'absolute pointer-events-none transition-opacity duration-200 bg-white dark:bg-neutral-800 p-2 rounded shadow-md text-sm z-10')
.style('opacity', 0)
}
// Create SVG
const svg = d3.select(heatmapContainer.value)
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
// Extract unique memory IDs
const memoryIds = Array.from(new Set(props.memoryData.map(m => m.id)))
// Get retrieval count data
const countData = memoryIds.map((id) => {
const memory = props.memoryData.find(m => m.id === id)
return {
id,
count: memory ? memory.retrieval_count : 0,
joyScore: memory ? memory.joy_score : 0,
aversionScore: memory ? memory.aversion_score : 0,
}
}).sort((a, b) => b.count - a.count) // Sort by count (highest first)
// Get sorted IDs for the y-axis
const sortedIds = countData.map(d => d.id)
// Set up scales
const yScale = d3.scaleBand()
.domain(sortedIds)
.range([0, height])
.padding(0.1)
// Find max count for color scale
const maxCount = d3.max(countData, d => d.count) || 1
// Create color scale for counts
const colorScale = d3.scaleSequential()
.domain([0, maxCount])
.interpolator(d3.interpolateReds)
// Create bar width scale
const xScale = d3.scaleLinear()
.domain([0, maxCount])
.range([0, width - 50]) // Leave space for count text
// Create y-axis (memory IDs)
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.attr('class', 'y-axis')
.call(yAxis)
// Create count bars
const bars = svg.selectAll('.count-bar')
.data(countData)
.enter()
.append('g')
.attr('class', 'count-bar')
.attr('transform', d => `translate(0, ${yScale(d.id)})`)
// Add bars
bars.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', d => xScale(d.count))
.attr('height', yScale.bandwidth())
.attr('fill', (d) => {
// Color based on emotional components and count
if (d.joyScore > 0.5 && d.count > 0)
return d3.interpolateYlOrRd(Math.min(1, d.joyScore * (d.count / maxCount)))
if (d.aversionScore > 0.5 && d.count > 0)
return d3.interpolatePurples(Math.min(1, d.aversionScore * (d.count / maxCount)))
return colorScale(d.count)
})
.attr('stroke', 'rgba(255, 255, 255, 0.3)')
.attr('stroke-width', 1)
.on('mouseover', function (event, d) {
d3.select(this)
.attr('stroke', '#fff')
.attr('stroke-width', 2)
const tooltipContent = `
<div class="font-semibold">${d.id}</div>
<div>Retrievals: ${d.count}</div>
<div>Joy: ${Math.round(d.joyScore * 100)}%</div>
<div>Aversion: ${Math.round(d.aversionScore * 100)}%</div>
`
tooltip.value
.style('opacity', 1)
.html(tooltipContent)
.style('left', `${event.offsetX + 10}px`)
.style('top', `${event.offsetY - 15}px`)
})
.on('mouseout', function () {
d3.select(this)
.attr('stroke', 'rgba(255, 255, 255, 0.3)')
.attr('stroke-width', 1)
tooltip.value
.style('opacity', 0)
})
// Add count labels
bars.append('text')
.attr('x', d => xScale(d.count) + 5)
.attr('y', yScale.bandwidth() / 2 + 5)
.attr('fill', 'currentColor')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.text(d => d.count)
// Add title
svg.append('text')
.attr('x', width / 2)
.attr('y', -10)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('fill', 'currentColor')
.text('Memory Retrieval Counts')
// Add legend
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 120}, -30)`)
// Legend items
const legendItems = [
{ label: 'Low', color: colorScale(maxCount * 0.2) },
{ label: 'Medium', color: colorScale(maxCount * 0.5) },
{ label: 'High', color: colorScale(maxCount * 0.8) },
{ label: 'Joy Impact', color: d3.interpolateYlOrRd(0.8) },
{ label: 'Aversion', color: d3.interpolatePurples(0.8) },
]
legendItems.forEach((item, i) => {
const g = legend.append('g')
.attr('transform', `translate(${(i % 3) * 70}, ${Math.floor(i / 3) * 20})`)
g.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', item.color)
g.append('text')
.attr('x', 20)
.attr('y', 12)
.attr('font-size', '10px')
.attr('fill', 'currentColor')
.text(item.label)
})
}
onMounted(() => {
// Setup resize observer
resizeObserver = new ResizeObserver(() => {
if (props.memoryData.length > 0) {
viewMode.value === 'patterns' ? renderHeatmap() : renderCountHeatmap()
}
})
if (heatmapContainer.value) {
resizeObserver.observe(heatmapContainer.value)
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
})
</script>
<template>
<div class="rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold">
Memory Retrieval Analysis
</h2>
<div class="flex rounded-lg bg-neutral-100 dark:bg-neutral-700">
<button
class="px-3 py-1 text-sm font-medium transition-colors"
:class="viewMode === 'patterns' ? 'bg-blue-500 text-white rounded-lg' : 'text-neutral-600 dark:text-neutral-300'"
@click="viewMode = 'patterns'"
>
Patterns
</button>
<button
class="px-3 py-1 text-sm font-medium transition-colors"
:class="viewMode === 'counts' ? 'bg-blue-500 text-white rounded-lg' : 'text-neutral-600 dark:text-neutral-300'"
@click="viewMode = 'counts'"
>
Counts
</button>
</div>
</div>
<div ref="heatmapContainer" class="relative h-[300px] w-full">
<!-- D3.js will render the heatmap here -->
</div>
<div v-if="viewMode === 'patterns'" class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Heatmap shows when memories were retrieved, with color intensity showing emotional impact.
Red outlines indicate possible random "flashback" retrievals.
</div>
<div v-else class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Bar chart shows total retrieval counts for each memory, sorted by frequency.
Color intensity and hue indicate emotional impact.
</div>
</div>
</template>
<style scoped>
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #dce0e3;
}
.dark {
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #374151;
}
}
</style>
@@ -1,190 +0,0 @@
<script setup lang="ts">
import type { MemoryItem } from '../../types/memory/memory-decay'
import { computed } from 'vue'
const props = defineProps<{
memory: MemoryItem
longTermMemoryEnabled: boolean
longTermMemoryThreshold: number
}>()
const emit = defineEmits(['retrieve'])
// Calculate age in days
const ageInDays = computed(() => {
return Math.round(props.memory.age_in_seconds / (24 * 60 * 60))
})
// Calculate memory status
const memoryStatus = computed(() => {
if (props.longTermMemoryEnabled && props.memory.retrieval_count >= props.longTermMemoryThreshold) {
return {
type: 'long-term',
label: `Long-term`,
color: 'text-purple-500 dark:text-purple-400',
bgColor: 'bg-purple-500 dark:bg-purple-400',
}
}
else if (props.memory.retrieval_count > 0) {
return {
type: 'working',
label: props.longTermMemoryEnabled
? `Working`
: 'Working memory',
color: 'text-blue-500 dark:text-blue-400',
bgColor: 'bg-blue-500 dark:bg-blue-400',
}
}
else {
return {
type: 'short-term',
label: 'Short-term',
color: 'text-red-500 dark:text-red-400',
bgColor: 'bg-red-500 dark:bg-red-400',
}
}
})
// Progress percentage for memory strength
const strengthPercentage = computed(() => {
return Math.round((props.memory.decayed_score / props.memory.score) * 100)
})
// Progress percentage for LTM
const ltmPercentage = computed(() => {
if (props.memory.retrieval_count >= props.longTermMemoryThreshold) {
return Math.round(Number.parseFloat(String(props.memory.ltm_factor || 0)) * 100)
}
else {
return Math.round((props.memory.retrieval_count / props.longTermMemoryThreshold) * 100)
}
})
function simulateRetrieval() {
emit('retrieve', props.memory.id)
}
</script>
<template>
<div class="flex flex-col justify-between rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<div class="flex justify-between">
<h3 class="font-bold">
{{ memory.id }}
</h3>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-xs font-medium dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800" h-fit
@click="simulateRetrieval"
>
Simulate Retrieval
</button>
</div>
<div>
<div class="grid grid-cols-2 mb-6 mt-2 gap-3" text-sm font-mono>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Phase:
</div>
<div class="font-medium" text-right text-nowrap :class="memoryStatus.color">
{{ memoryStatus.label }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Original Score:
</div>
<div class="font-medium" text-right>
{{ Math.round(memory.score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Current Score:
</div>
<div class="font-medium" text-right>
{{ Math.round(memory.decayed_score) }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
% Remaining:
</div>
<div class="font-medium" text-right>
{{ strengthPercentage }}%
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Creation Date:
</div>
<div class="font-medium" text-right>
{{ new Date(memory.updated_at).toLocaleDateString() }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Last Retrieved:
</div>
<div class="font-medium" text-right>
{{ new Date(memory.last_retrieved_at).toLocaleDateString() }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Retrieval Count:
</div>
<div class="font-medium" text-right>
{{ memory.retrieval_count }}
</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Age:
</div>
<div class="font-medium" text-right>
{{ ageInDays }} days
</div>
</div>
</div>
<div>
<!-- Visual representation of decay -->
<div class="mt-4" font-mono>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Memory Strength:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
class="h-full max-w-full transition-all duration-500"
:class="longTermMemoryEnabled && memory.retrieval_count >= longTermMemoryThreshold ? 'bg-purple-500 dark:bg-purple-400' : 'bg-blue-500 dark:bg-blue-400'"
:style="`width: ${strengthPercentage}%`"
/>
</div>
<div class="mt-1 flex justify-between text-xs">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
</div>
<!-- LTM Progress -->
<div v-if="longTermMemoryEnabled && memory.retrieval_count > 0" class="mt-4" font-mono>
<div class="mb-1 text-sm text-neutral-500 dark:text-neutral-400" tracking-tight>
Long-term Memory Progress:
</div>
<div class="h-7 overflow-hidden rounded-lg bg-neutral-200 dark:bg-neutral-600">
<div
:class="`h-full max-w-full transition-all duration-500 ${memoryStatus.bgColor}`"
:style="`width: ${ltmPercentage}%`"
/>
</div>
<div class="mt-1 flex items-center justify-between">
<span class="text-xs">Short-term</span>
<span
v-if="memory.retrieval_count < longTermMemoryThreshold"
:class="`text-xs font-medium ${memoryStatus.color}`"
>
Working ({{ memory.retrieval_count }}/{{ longTermMemoryThreshold }})
</span>
<span v-else :class="`text-sm font-medium ${memoryStatus.color}`">
{{ Math.round(Number.parseFloat(String(memory.ltm_factor || 0)) * 100) }}% stable
</span>
<span class="text-xs">Permanent</span>
</div>
</div>
</div>
</div>
</template>
@@ -1,575 +0,0 @@
<script setup lang="ts">
import type { MemoryItem } from '../../types/memory/memory-decay'
import { useDark } from '@vueuse/core'
import * as d3 from 'd3'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
const props = defineProps<{
memoryData: MemoryItem[]
selectedStoryId: string
decayRate: number
maxDaysToShow: number
longTermMemoryEnabled: boolean
longTermMemoryThreshold: number
longTermMemoryVisualize: boolean
retrievalBoost: number
retrievalDecaySlowdown: number
}>()
interface DataPoint {
x: number
y: number
label: string
}
interface ChartData {
labels: string[]
dataPoints: {
withRetrievals: DataPoint[]
withoutRetrievals: DataPoint[]
ltmProjection: DataPoint[]
}
}
const chartContainer = ref(null)
const tooltip = ref(null)
let resizeObserver = null
const isDark = useDark()
// Render chart when dependencies change
watchEffect(() => {
if (chartContainer.value && props.memoryData.length && props.selectedStoryId) {
renderChart()
}
})
// Render D3 chart
function renderChart() {
// Clear existing chart
d3.select(chartContainer.value).selectAll('*').remove()
// Find the selected story
const story = props.memoryData.find(s => s.id === props.selectedStoryId)
if (!story)
return
// Get container dimensions
const containerRect = chartContainer.value.getBoundingClientRect()
const containerWidth = containerRect.width
const containerHeight = containerRect.height || 320
// Set chart margins
const margin = { top: 20, right: 0, bottom: 40, left: 50 }
const width = containerWidth - margin.left - margin.right
const height = containerHeight - margin.top - margin.bottom
// Prepare data for chart
const originalScore = Number.parseFloat(String(story.score))
const dr = props.decayRate
const maxDays = props.maxDaysToShow
const ageInDays = Number.parseFloat(String(story.age_in_seconds)) / (24 * 60 * 60)
const retrievals = Number.parseInt(String(story.retrieval_count))
const daysSinceRetrieval = Number.parseFloat(String(story.time_since_retrieval)) / (24 * 60 * 60)
const ltmFactor = props.longTermMemoryEnabled ? Number.parseFloat(String(story.ltm_factor || '0')) : 0
// Generate data points
const chartData = {
labels: ['Original', 'Current'],
dataPoints: {
withRetrievals: [
{ x: 0, y: originalScore, label: 'Original' },
{ x: ageInDays, y: Number.parseFloat(String(story.decayed_score)), label: 'Current' },
],
withoutRetrievals: [
{ x: 0, y: originalScore, label: 'Original' },
{ x: ageInDays, y: originalScore * Math.exp(-dr * ageInDays * (1 - ltmFactor)), label: 'Current' },
],
ltmProjection: props.longTermMemoryVisualize
? [
{ x: 0, y: originalScore, label: 'Original' },
{
x: ageInDays,
y: retrievals >= props.longTermMemoryThreshold
? originalScore * (1 - Math.max(0, 0.1 - 0.1 * retrievals))
: Number.parseFloat(String(story.decayed_score)),
label: 'Current',
},
]
: [],
},
}
// Add future projection points
const dayStep = Math.ceil(maxDays / 10)
for (let day = dayStep; day <= maxDays; day += dayStep) {
const futureDay = ageInDays + day
const futureDaysSinceRetrieval = daysSinceRetrieval + day
const label = `+${day}d`
// Point with retrieval effects
const scoreWithRetrievals = originalScore
* Math.exp(-dr * futureDay * (1 - ltmFactor))
* (1 + (retrievals * props.retrievalBoost
* Math.exp(-dr * props.retrievalDecaySlowdown * futureDaysSinceRetrieval)))
// Point without retrieval effects
const scoreWithoutRetrievals = originalScore * Math.exp(-dr * futureDay * (1 - ltmFactor))
// Add to data series
chartData.labels.push(label)
chartData.dataPoints.withRetrievals.push({ x: futureDay, y: scoreWithRetrievals, label })
chartData.dataPoints.withoutRetrievals.push({ x: futureDay, y: scoreWithoutRetrievals, label })
// Add LTM projection if enabled
if (props.longTermMemoryVisualize) {
const ltmProjectionValue = retrievals >= props.longTermMemoryThreshold
? originalScore * (1 - Math.max(0, 0.1 - 0.1 * retrievals)) * Math.exp(-0.01 * day)
: scoreWithRetrievals
chartData.dataPoints.ltmProjection.push({ x: futureDay, y: ltmProjectionValue, label })
}
}
// Create tooltip if it doesn't exist
if (!tooltip.value) {
tooltip.value = d3.select(chartContainer.value)
.append('div')
.attr('class', 'absolute pointer-events-none transition-opacity duration-200 bg-white dark:bg-neutral-800 p-2 rounded shadow-md text-sm z-10')
.style('opacity', 0)
}
// Create SVG
const svg = d3.select(chartContainer.value)
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
// Set up scales
const xExtent = [0, maxDays + ageInDays]
const yExtent = [0, d3.max([
...chartData.dataPoints.withRetrievals,
...chartData.dataPoints.withoutRetrievals,
...chartData.dataPoints.ltmProjection,
], d => d.y) * 1.1]
const xScale = d3.scaleLinear()
.domain(xExtent)
.range([0, width])
const yScale = d3.scaleLinear()
.domain(yExtent)
.range([height, 0])
// Add axes and gridlines
addAxesAndGrids(svg, xScale, yScale, width, height)
// Add data lines and areas
addDataLines(svg, chartData, xScale, yScale, height)
// Add half-life indicator
const halfLife = Math.log(2) / dr
if (halfLife <= maxDays + ageInDays) {
addHalfLifeIndicator(svg, halfLife, xScale, height)
}
// Add data points with interaction
addDataPoints(svg, chartData.dataPoints.withRetrievals, xScale, yScale)
// Add chart title and legends
addTitleAndLegends(svg, story.id, retrievals, ltmFactor, width)
// Add LTM indicators
if (props.longTermMemoryEnabled) {
addLTMIndicators(svg, retrievals, props.longTermMemoryThreshold, ltmFactor, width)
}
}
function addAxesAndGrids(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
width: number,
height: number,
): void {
const xAxis = d3.axisBottom(xScale)
.ticks(5)
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())} days`)
const yAxis = d3.axisLeft(yScale)
.ticks(5)
.tickFormat((d: d3.NumberValue) => `${Math.round(d.valueOf())}`)
// Add gridlines
if (!isDark.value) {
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(() => ''), // Use a function that returns empty string
)
.selectAll('line')
.attr('stroke', 'red')
.attr('stroke-opacity', 0.5)
svg.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(() => ''), // Use a function that returns empty string
)
.selectAll('line')
.attr('stroke', 'green')
.attr('stroke-opacity', 0.5)
}
else {
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(() => ''), // Use a function that returns empty string
)
.selectAll('line')
.attr('stroke', 'yellow')
.attr('stroke-opacity', 0.5)
svg.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(() => ''), // Use a function that returns empty string
)
.selectAll('line')
.attr('stroke', 'blue')
.attr('stroke-opacity', 0.5)
}
// Add axes
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.append('text')
.attr('class', 'axis-label')
.attr('x', width / 2)
.attr('y', 36)
.attr('text-anchor', 'middle')
.text('Time (days)')
.attr('fill', 'currentColor')
svg.append('g')
.attr('class', 'y-axis')
.call(yAxis)
.append('text')
.attr('class', 'axis-label')
.attr('transform', 'rotate(-90)')
.attr('y', -36)
.attr('x', -height / 2)
.attr('text-anchor', 'middle')
.text('Memory Strength')
.attr('fill', 'currentColor')
}
function addDataLines(
svg: d3.Selection<SVGGElement, unknown, null, undefined>,
chartData: ChartData,
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
height: number,
): void {
// Create line generators with correct types
const line = d3.line<DataPoint>()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX)
// Create area generators with correct types
const area = d3.area<DataPoint>()
.x(d => xScale(d.x))
.y0(height)
.y1(d => yScale(d.y))
.curve(d3.curveMonotoneX)
// Add area fill for without retrievals
svg.append('path')
.datum(chartData.dataPoints.withoutRetrievals)
.attr('class', 'area without-retrievals')
.attr('d', area)
.attr('fill', 'rgba(156, 163, 175, 0.2)')
// Add line for without retrievals
svg.append('path')
.datum(chartData.dataPoints.withoutRetrievals)
.attr('class', 'line without-retrievals')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(156, 163, 175, 0.6)')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,3')
// Add area fill for with retrievals
svg.append('path')
.datum(chartData.dataPoints.withRetrievals)
.attr('class', 'area with-retrievals')
.attr('d', area)
.attr('fill', 'rgba(59, 130, 246, 0.2)')
// Add line for with retrievals
svg.append('path')
.datum(chartData.dataPoints.withRetrievals)
.attr('class', 'line with-retrievals')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(59, 130, 246, 1)')
.attr('stroke-width', 3)
// Add LTM projection line if enabled
if (props.longTermMemoryVisualize && chartData.dataPoints.ltmProjection.length > 0) {
svg.append('path')
.datum(chartData.dataPoints.ltmProjection)
.attr('class', 'line ltm-projection')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'rgba(139, 92, 246, 0.8)')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '3,2')
}
}
function addDataPoints(svg: d3.Selection<SVGGElement, unknown, null, undefined>, dataPoints: DataPoint[], xScale: d3.ScaleLinear<number, number>, yScale: d3.ScaleLinear<number, number>) {
svg.selectAll('.point-with-retrievals')
.data(dataPoints)
.enter()
.append('circle')
.attr('class', 'point-with-retrievals')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('r', (d, i) => i < 2 ? 6 : 5)
.attr('fill', (d, i) => i < 2 ? 'rgba(220, 38, 38, 0.8)' : 'rgba(59, 130, 246, 0.8)')
.attr('stroke', 'white')
.attr('stroke-width', 2)
.on('mouseover', function (event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
tooltip.value
.style('opacity', 1)
.html(`
<div class="font-semibold">${d.label}</div>
<div>Score: ${Math.round(d.y)}</div>
<div>Day: ${Math.round(d.x)}</div>
`)
.style('left', `${event.offsetX + 15}px`)
.style('top', `${event.offsetY - 28}px`)
})
.on('mouseout', function () {
d3.select(this)
.transition()
.duration(200)
.attr('r', (d, i) => i < 2 ? 6 : 5)
tooltip.value
.transition()
.duration(200)
.style('opacity', 0)
})
// Add labels to important points
svg.selectAll('.point-label')
.data(dataPoints.filter((d, i) => i === 0 || i === 1 || i === dataPoints.length - 1 || i % 3 === 0))
.enter()
.append('text')
.attr('class', 'point-label')
.attr('x', d => xScale(d.x))
.attr('y', d => yScale(d.y) - 15)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('fill', 'currentColor')
.text(d => Math.round(d.y))
}
function addHalfLifeIndicator(svg: d3.Selection<SVGGElement, unknown, null, undefined>, halfLife: number, xScale: d3.ScaleLinear<number, number>, height: number) {
svg.append('line')
.attr('class', 'half-life-line')
.attr('x1', xScale(halfLife))
.attr('y1', 0)
.attr('x2', xScale(halfLife))
.attr('y2', height)
.attr('stroke', 'rgba(220, 38, 38, 0.5)')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5')
svg.append('text')
.attr('class', 'half-life-label')
.attr('x', xScale(halfLife))
.attr('y', 0)
.attr('dy', -8)
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('fill', 'rgba(220, 38, 38, 0.8)')
.text(`Half-life: ${halfLife.toFixed(1)} days`)
}
function addTitleAndLegends(svg: d3.Selection<SVGGElement, unknown, null, undefined>, storyId: string, retrievals: number, ltmFactor: number, width: number) {
// Add chart title
let titleText = `Memory Decay for ${storyId} (${retrievals} retrievals)`
if (props.longTermMemoryEnabled) {
if (retrievals >= props.longTermMemoryThreshold) {
const ltmPercent = Math.round(ltmFactor * 100)
titleText += ` - LTM: ${ltmPercent}%`
}
else if (retrievals > 0) {
titleText += ` - LTM: ${retrievals}/${props.longTermMemoryThreshold} retrievals`
}
}
svg.append('text')
.attr('class', 'chart-title')
.attr('x', width / 2)
.attr('y', -30)
.attr('text-anchor', 'middle')
.attr('font-size', '16px')
.attr('font-weight', 'bold')
.attr('fill', 'currentColor')
.text(titleText)
// Add legend
const legendData = [
{ label: `Current (${retrievals} retrievals)`, color: 'rgba(59, 130, 246, 0.8)' },
{ label: 'Without retrievals', color: 'rgba(156, 163, 175, 0.8)' },
]
if (props.longTermMemoryVisualize) {
legendData.push({ label: 'Long-term memory', color: 'rgba(139, 92, 246, 0.8)' })
}
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 180}, 10)`)
const legendItems = legend.selectAll('.legend-item')
.data(legendData)
.enter()
.append('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(10, ${i * 25 + 20})`)
legendItems.append('circle')
.attr('r', 6)
.attr('fill', d => d.color)
.attr('stroke', 'white')
.attr('stroke-width', 1)
legendItems.append('text')
.attr('x', 15)
.attr('y', 4)
.attr('font-size', '12px')
.attr('fill', 'currentColor')
.text(d => d.label)
}
function addLTMIndicators(svg: d3.Selection<SVGGElement, unknown, null, undefined>, retrievals: number, threshold: number, ltmFactor: number, width: number) {
if (retrievals > 0 && retrievals < threshold) {
const progress = Math.round((retrievals / threshold) * 100)
svg.append('g')
.attr('class', 'progress-indicator')
.attr('transform', `translate(${width / 2}, ${-20})`)
.append('text')
.attr('text-anchor', 'middle')
.attr('font-size', '12px')
.attr('fill', 'rgba(139, 92, 246, 0.8)')
.text(`LTM Progress: ${progress}%`)
}
if (retrievals >= threshold) {
const stability = Math.round(ltmFactor * 100)
svg.append('text')
.attr('class', 'ltm-info')
.attr('x', width / 2)
.attr('y', -10)
.attr('text-anchor', 'middle')
.attr('font-size', '14px')
.attr('fill', 'rgba(139, 92, 246, 1)')
.text(`Long-term memory stability: ${stability}%`)
}
}
onMounted(() => {
// Setup resize observer
resizeObserver = new ResizeObserver(() => {
if (props.memoryData.length && props.selectedStoryId) {
renderChart()
}
})
if (chartContainer.value) {
resizeObserver.observe(chartContainer.value)
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
})
</script>
<template>
<div class="rounded-lg bg-white p-4 shadow dark:bg-neutral-800/50">
<h2 class="mb-4 text-xl font-semibold">
Memory Strength Projection
</h2>
<!-- D3.js chart container -->
<div ref="chartContainer" class="relative h-120 w-full">
<!-- D3 will render the chart here -->
</div>
</div>
</template>
<style scoped>
/* D3 Chart Styles */
:deep(.axis-label) {
font-size: 12px;
}
:deep(.y-axis .tick text) {
transform: rotate(-90deg) translateY(-20px);
}
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #dce0e3;
}
:deep(.grid line) {
stroke: #2e3032;
stroke-opacity: 0.5;
}
.dark {
:deep(.x-axis path),
:deep(.y-axis path),
:deep(.x-axis line),
:deep(.y-axis line),
:deep(.domain) {
stroke: #2e3032;
}
}
</style>
@@ -1,177 +0,0 @@
<!-- src/components/MemoryTable.vue -->
<script setup lang="ts">
interface MemoryDataItem {
id: string
score: number
updated_at: string // ISO timestamp
last_retrieved_at: string // ISO timestamp
retrieval_count: number
updated_at_str: string
last_retrieved_str: string
age_in_seconds: number
time_since_retrieval: number
ltm_factor?: number // Optional since it's only present when longTermMemoryEnabled is true
decayed_score: number
}
const props = defineProps<{
memories: MemoryDataItem[]
selectedId: string | null
longTermMemoryEnabled: boolean
longTermMemoryThreshold: number
}>()
const emit = defineEmits(['select', 'retrieve'])
function selectMemory(id) {
emit('select', id)
}
function retrieveMemory(id, event) {
event.stopPropagation()
emit('retrieve', id)
}
function getMemoryStatus(memory) {
if (props.longTermMemoryEnabled && memory.retrieval_count >= props.longTermMemoryThreshold) {
return {
type: 'long-term',
label: `Long-term (${Math.round((memory.ltm_factor || 0) * 100)}%)`,
class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
}
}
else if (memory.retrieval_count > 0) {
return {
type: 'working',
label: props.longTermMemoryEnabled ? `Working (${memory.retrieval_count}/${props.longTermMemoryThreshold})` : 'Working',
class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
}
}
else {
return {
type: 'short-term',
label: 'Short-term',
class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
}
}
}
</script>
<template>
<div class="rounded-lg bg-neutral-50 shadow dark:bg-neutral-800/50">
<div class="border-b p-4 dark:border-neutral-700">
<h2 class="text-xl font-semibold">
All Memory Items
</h2>
<p class="mt-1 text-sm text-neutral-500">
Items are ranked by current memory strength (click to analyze or simulate retrieval)
</p>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
<thead class="bg-neutral-100 dark:bg-neutral-800">
<tr>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Rank
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Memory ID
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Original Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Age (Days)
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Retrievals
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Last Retrieved
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Memory Status
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Current Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-neutral-200 dark:bg-neutral-900 dark:divide-neutral-800">
<tr
v-for="(item, idx) in memories" :key="item.id"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': item.id === selectedId }"
class="cursor-pointer transition duration-150 hover:bg-neutral-50 dark:hover:bg-neutral-800"
@click="selectMemory(item.id)"
>
<td class="whitespace-nowrap px-4 py-2 text-sm font-medium">
{{ idx + 1 }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ item.id }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ Math.round(item.score) }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ Math.round(item.age_in_seconds / (24 * 60 * 60)) }}
</td>
<td
class="whitespace-nowrap px-4 py-2 text-sm font-medium"
:class="item.retrieval_count > 0 ? 'text-green-600 dark:text-green-400' : ''"
>
{{ item.retrieval_count }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ item.retrieval_count > 0 ? new Date(item.last_retrieved_at).toLocaleDateString() : '-' }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
<span
class="rounded px-2 py-1 text-xs font-medium"
:class="getMemoryStatus(item).class"
>
{{ getMemoryStatus(item).label }}
</span>
</td>
<td class="whitespace-nowrap px-4 py-2">
<div class="flex items-center">
<div class="w-16 text-sm">
{{ Math.round(item.decayed_score) }}
</div>
<div class="ml-2 h-2 w-24 rounded-full bg-neutral-200 dark:bg-neutral-700">
<div
class="h-full max-w-full rounded-full"
:class="longTermMemoryEnabled && item.retrieval_count >= longTermMemoryThreshold ? 'bg-purple-500' : 'bg-blue-500'"
:style="`width: ${Math.round((item.decayed_score / item.score) * 100)}%`"
/>
</div>
</div>
</td>
<td class="whitespace-nowrap px-4 py-2 text-right">
<button
class="rounded-lg bg-green-100 px-3 py-1 text-xs font-medium dark:bg-green-900 hover:bg-green-200 dark:hover:bg-green-800"
@click="retrieveMemory(item.id, $event)"
>
Retrieve
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
@media (max-width: 768px) {
:deep(table) {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
</style>
@@ -1,328 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
const props = withDefaults(defineProps<{
min?: number
max?: number
step?: number
disabled?: boolean
thumbColor?: string
trackColor?: string
trackValueColor?: string
}>(), {
min: 0,
max: 100,
step: 1,
disabled: false,
thumbColor: '#9090906e',
trackColor: 'gray',
trackValueColor: 'red',
})
const modelValue = defineModel<number>('modelValue', { required: true })
const scaledMin = computed(() => props.min * 10000)
const scaledMax = computed(() => props.max * 10000)
const scaledStep = computed(() => props.step * 10000)
const sliderRef = ref<HTMLInputElement>()
const sliderValue = computed({
get: () => modelValue.value * 10000,
set: (value: number) => {
modelValue.value = value / 10000
updateTrackColor()
},
})
onMounted(() => {
updateTrackColor()
})
function updateTrackColor() {
if (!sliderRef.value)
return
sliderRef.value.style.setProperty('--value', sliderRef.value.value)
sliderRef.value.style.setProperty('--min', !sliderRef.value.min ? props.min.toString() : sliderRef.value.min)
sliderRef.value.style.setProperty('--max', !sliderRef.value.max ? props.max.toString() : sliderRef.value.max)
}
</script>
<template>
<input
ref="sliderRef"
v-model.number="sliderValue"
type="range"
:min="scaledMin"
:max="scaledMax"
:step="scaledStep"
class="slider-progress form_input-range"
@input="(e) => {
(e.target as HTMLInputElement).style.setProperty('--value', (e.target as HTMLInputElement).value)
}"
>
</template>
<style scoped>
/*generated with Input range slider CSS style generator (version 20211225)
https://toughengineer.github.io/demo/slider-styler*/
.form_input-range {
--height: 2em;
min-height: var(--height);
appearance: none;
background: transparent;
border-radius: 4px;
transition: background-color 0.2s ease;
--thumb-width: 4px;
--thumb-height: var(--height);
--thumb-box-shadow: 0 0 0px #e6e6e6;
--thumb-border: none;
--thumb-border-radius: 999px;
--thumb-background: oklch(80% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--thumb-background-hover: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--thumb-background-active: oklch(70% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--track-height: calc(var(--height) - var(--track-value-padding) * 2);
--track-box-shadow: none;
--track-border: solid 2px rgb(238, 238, 238);
--track-border-radius: 6px;
--track-background: rgb(238, 238, 238);
--track-background-hover: rgb(238, 238, 238);
--track-background-active: rgb(238, 238, 238);
--track-value-background: rgb(255, 255, 255);
--track-value-background-hover: rgb(255, 255, 255);
--track-value-background-active: rgb(255, 255, 255);
--track-value-padding: 2px;
}
.dark .form_input-range {
--thumb-background: oklch(70% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--thumb-background-hover: oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--thumb-background-active: oklch(80% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + 0));
--track-border: solid 2px rgb(44, 44, 44);
--track-background: rgb(44, 44, 44);
--track-background-hover: rgb(44, 44, 44);
--track-background-active: rgb(44, 44, 44);
--track-value-background: rgb(164, 164, 164);
--track-value-background-hover: rgb(164, 164, 164);
--track-value-background-active: rgb(164, 164, 164);
}
/*progress support*/
.form_input-range.slider-progress {
--range: calc(var(--max) - var(--min));
--ratio: calc((var(--value) - var(--min)) / var(--range));
--sx: calc(0.5 * 0em + var(--ratio) * (100% - 0em));
}
.form_input-range:focus {
outline: none;
}
/*webkit*/
.form_input-range::-webkit-slider-thumb {
appearance: none;
width: var(--thumb-width);
height: var(--thumb-height);
border-radius: var(--thumb-border-radius);
background: var(--thumb-background);
border: var(--thumb-border);
box-shadow: var(--thumb-box-shadow);
margin-top: calc(var(--track-height) * 0.5 - var(--thumb-height) * 0.5 - 2px);
margin-left: calc(0 - var(--track-value-padding));
cursor: col-resize;
transition:
background 0.2s ease-in-out,
box-shadow 0.2s ease-in-out,
border-color 0.2s ease-in-out,
transform 0.2s ease-in-out;
}
.form_input-range::-webkit-slider-runnable-track {
height: var(--track-height);
border: var(--track-border);
border-radius: var(--track-border-radius);
background: var(--track-background);
box-shadow: var(--track-box-shadow);
position: relative;
cursor: col-resize;
transition:
box-shadow 0.2s ease-in-out,
border-color 0.2s ease-in-out;
}
.form_input-range::-webkit-slider-thumb:hover {
background: var(--thumb-background-hover);
}
.form_input-range:hover::-webkit-slider-runnable-track {
background: var(--track-background-hover);
}
.form_input-range::-webkit-slider-thumb:active {
background: var(--thumb-background-active);
}
.form_input-range:active::-webkit-slider-runnable-track {
background: var(--track-background-active);
}
.form_input-range.slider-progress::-webkit-slider-runnable-track {
/* margin-left: var(--track-value-padding); */
margin-right: calc(0 - var(--track-value-padding));
background:
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
var(--track-background);
}
.form_input-range.slider-progress:hover::-webkit-slider-runnable-track {
background:
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
no-repeat,
var(--track-background-hover);
}
.form_input-range.slider-progress:active::-webkit-slider-runnable-track {
background:
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
no-repeat,
var(--track-background-active);
}
/*mozilla*/
.form_input-range::-moz-range-thumb {
width: var(--thumb-width);
height: var(--thumb-height);
border-radius: var(--thumb-border-radius);
background: var(--thumb-background);
border: none;
box-shadow: var(--thumb-box-shadow);
cursor: col-resize;
margin-left: calc(0 - var(--track-value-padding));
}
.form_input-range::-moz-range-track {
height: var(--track-height);
border: var(--track-border);
border-radius: var(--track-border-radius);
background: var(--track-background);
box-shadow: var(--track-box-shadow);
cursor: col-resize;
/* Trim left and right paddings of track */
width: calc(100% - var(--track-value-padding) * 2);
}
.form_input-range::-moz-range-thumb:hover {
background: var(--thumb-background-hover);
}
.form_input-range:hover::-moz-range-track {
background: var(--track-background-hover);
}
.form_input-range::-moz-range-thumb:active {
background: var(--thumb-background-active);
}
.form_input-range:active::-moz-range-track {
background: var(--track-background-active);
}
.form_input-range.slider-progress::-moz-range-track {
background:
linear-gradient(var(--track-value-background), var(--track-value-background)) 0 / var(--sx) 100% no-repeat,
var(--track-background);
}
.form_input-range.slider-progress:hover::-moz-range-track {
background:
linear-gradient(var(--track-value-background-hover), var(--track-value-background-hover)) 0 / var(--sx) 100%
no-repeat,
var(--track-background-hover);
}
.form_input-range.slider-progress:active::-moz-range-track {
background:
linear-gradient(var(--track-value-background-active), var(--track-value-background-active)) 0 / var(--sx) 100%
no-repeat,
var(--track-background-active);
}
/*ms*/
.form_input-range::-ms-fill-upper {
background: transparent;
border-color: transparent;
}
.form_input-range::-ms-fill-lower {
background: transparent;
border-color: transparent;
}
.form_input-range::-ms-thumb {
width: var(--thumb-width);
height: var(--thumb-height);
border-radius: var(--thumb-border-radius);
background: var(--thumb-background);
border: var(--thumb-border);
box-shadow: var(--thumb-box-shadow);
/** Center thumb */
margin-top: 0;
/** Shift left thumb */
margin-left: calc(0 - var(--track-value-padding));
box-sizing: border-box;
cursor: col-resize;
}
.form_input-range::-ms-track {
height: var(--track-height);
border-radius: var(--track-border-radius);
background: var(--track-background);
border: var(--track-border);
box-shadow: var(--track-box-shadow);
box-sizing: border-box;
cursor: col-resize;
}
.form_input-range::-ms-thumb:hover {
background: var(--thumb-background-hover);
}
.form_input-range:hover::-ms-track {
background: var(--track-background-hover);
}
.form_input-range::-ms-thumb:active {
background: var(--thumb-background-active);
}
.form_input-range:active::-ms-track {
background: var(--track-background-active);
}
.form_input-range.slider-progress::-ms-fill-lower {
height: var(--track-height);
border-radius: var(--track-border-radius) 0 0 var(--track-border-radius);
margin: 0;
background: var(--track-value-background);
border: none;
border-right-width: 0;
/** Shift left thumb */
margin-left: calc(var(--track-value-padding));
/** Shift right thumb */
margin-right: calc(0 - var(--track-value-padding));
}
.form_input-range.slider-progress:hover::-ms-fill-lower {
background: var(--track-value-background-hover);
}
.form_input-range.slider-progress:active::-ms-fill-lower {
background: var(--track-value-background-active);
}
</style>
@@ -1,262 +0,0 @@
import { buildDSN } from '../../../../src/dsn'
import { drizzle } from '../../../../src/index'
import * as schema from '../../../db/schema'
export async function connectToDatabase() {
return drizzle(buildDSN({
scheme: 'duckdb-wasm:',
bundles: 'import-url',
logger: false,
}), { schema })
}
export async function createSchema(db) {
await db.execute(`
CREATE TABLE IF NOT EXISTS memories_decay_test_table (
id VARCHAR,
score DOUBLE,
updated_at TIMESTAMP,
last_retrieved_at TIMESTAMP,
retrieval_count INTEGER DEFAULT 0
)
`)
}
export async function loadSampleData(db) {
const now = new Date()
const sampleData = []
for (let i = 1; i <= 20; i++) {
const daysAgo = Math.random() * 60
const lastUpdate = new Date(now.getTime() - (daysAgo * 24 * 60 * 60 * 1000))
const retrievalCount = Math.floor(Math.random() * 10)
let retrievalDaysAgo = retrievalCount > 0 ? Math.random() * daysAgo * (1 - retrievalCount / 15) : daysAgo
retrievalDaysAgo = Math.max(0, retrievalDaysAgo)
const lastRetrievedAt = new Date(now.getTime() - (retrievalDaysAgo * 24 * 60 * 60 * 1000))
const score = Math.floor(Math.random() * 900) + 100
sampleData.push({
id: `story-${i}`,
score,
updated_at: lastUpdate.toISOString(),
last_retrieved_at: lastRetrievedAt.toISOString(),
retrieval_count: retrievalCount,
})
}
for (const item of sampleData) {
await db.execute(`
INSERT INTO memories_decay_test_table (id, score, updated_at, last_retrieved_at, retrieval_count)
VALUES ('${item.id}', ${item.score}, '${item.updated_at}', '${item.last_retrieved_at}', ${item.retrieval_count})
`)
}
}
export async function generateDecayQuery(db, {
simulatedTimeOffset,
decayRate,
timeUnitInSeconds,
longTermMemoryEnabled,
longTermMemoryThreshold,
longTermMemoryStability,
retrievalBoost,
retrievalDecaySlowdown,
}) {
const simulatedTimestamp = `(CAST(now() AS TIMESTAMP) + INTERVAL '${simulatedTimeOffset} seconds')`
const ltmFactorClause = longTermMemoryEnabled
? `
CASE
WHEN retrieval_count >= ${longTermMemoryThreshold}
THEN 1.0 - ((${longTermMemoryStability}) ^ (retrieval_count / ${longTermMemoryThreshold}))
ELSE 0
END AS ltm_factor,
`
: ''
const ltmDecayModifier = longTermMemoryEnabled
? `* (1 - ltm_factor)`
: ''
const query = `
SELECT
id,
score,
updated_at,
last_retrieved_at,
retrieval_count,
CAST(updated_at AS VARCHAR) as updated_at_str,
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str,
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at))) as age_in_seconds,
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at))) as time_since_retrieval,
${ltmFactorClause}
score *
exp(-${decayRate} * (EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at)))/(${timeUnitInSeconds}) ${ltmDecayModifier}) *
(1 + (retrieval_count * ${retrievalBoost} *
exp(-${decayRate * retrievalDecaySlowdown} *
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds}))))
as decayed_score
FROM memories_decay_test_table
ORDER BY decayed_score DESC
`
return query
}
export async function simulateRetrieval(db, storyId, simulatedTime) {
const simulatedTimestamp = simulatedTime.toISOString()
await db.execute(`
UPDATE memories_decay_test_table
SET last_retrieved_at = '${simulatedTimestamp}',
retrieval_count = retrieval_count + 1
WHERE id = '${storyId}'
`)
}
export async function generateEmotionalDecayQuery(db, {
simulatedTimeOffset,
decayRate,
timeUnitInSeconds,
longTermMemoryEnabled,
longTermMemoryThreshold,
longTermMemoryStability,
retrievalBoost,
retrievalDecaySlowdown,
joyBoostFactor,
joyDecaySteepness,
aversionSpikeFactor,
aversionStability,
randomRecallProbability,
flashbackIntensity,
}) {
const simulatedTimestamp = `(CAST(now() AS TIMESTAMP) + INTERVAL '${simulatedTimeOffset} seconds')`
const ltmFactorClause = longTermMemoryEnabled
? `
CASE
WHEN retrieval_count >= ${longTermMemoryThreshold}
THEN 1.0 - ((${longTermMemoryStability}) ^ (retrieval_count / ${longTermMemoryThreshold}))
ELSE 0
END AS ltm_factor,
`
: ''
const ltmDecayModifier = longTermMemoryEnabled
? `* (1 - ltm_factor)`
: ''
// New emotional memory components
const emotionalComponents = `
-- Random recall probability (flashback effect)
(CASE WHEN RANDOM() < ${randomRecallProbability} THEN ${flashbackIntensity} ELSE 1 END) *
-- Joy/euphoria boost with steep decay
(1 + (joy_score * ${joyBoostFactor} * EXP(-${joyDecaySteepness} *
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds})))) *
-- Aversion spike for traumatic memories
(1 + (aversion_score * ${aversionSpikeFactor} *
POWER(${aversionStability}, CEIL((retrieval_count / 5)))))
`
const query = `
SELECT
id,
score,
updated_at,
last_retrieved_at,
retrieval_count,
joy_score,
aversion_score,
CAST(updated_at AS VARCHAR) as updated_at_str,
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str,
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at))) as age_in_seconds,
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at))) as time_since_retrieval,
${ltmFactorClause}
score *
exp(-${decayRate} * (EXTRACT(EPOCH FROM (${simulatedTimestamp} - updated_at)))/(${timeUnitInSeconds}) ${ltmDecayModifier}) *
(1 + (retrieval_count * ${retrievalBoost} *
exp(-${decayRate * retrievalDecaySlowdown} *
(EXTRACT(EPOCH FROM (${simulatedTimestamp} - last_retrieved_at)))/(${timeUnitInSeconds})))) *
${emotionalComponents} as decayed_score
FROM emotional_memories_test_table
ORDER BY decayed_score DESC
`
return query
}
export async function createEmotionalSchema(db) {
await db.execute(`
CREATE TABLE IF NOT EXISTS emotional_memories_test_table (
id VARCHAR,
score DOUBLE,
updated_at TIMESTAMP,
last_retrieved_at TIMESTAMP,
retrieval_count INTEGER DEFAULT 0,
joy_score DOUBLE DEFAULT 0,
aversion_score DOUBLE DEFAULT 0
)
`)
}
export async function loadEmotionalSampleData(db) {
const now = new Date()
const sampleData = []
for (let i = 1; i <= 20; i++) {
const daysAgo = Math.random() * 60
const lastUpdate = new Date(now.getTime() - (daysAgo * 24 * 60 * 60 * 1000))
const retrievalCount = Math.floor(Math.random() * 10)
let retrievalDaysAgo = retrievalCount > 0 ? Math.random() * daysAgo * (1 - retrievalCount / 15) : daysAgo
retrievalDaysAgo = Math.max(0, retrievalDaysAgo)
const lastRetrievedAt = new Date(now.getTime() - (retrievalDaysAgo * 24 * 60 * 60 * 1000))
const score = Math.floor(Math.random() * 900) + 100
// Generate emotional scores
const joyScore = Math.random() * (i % 3 === 0 ? 0.8 : 0.3)
const aversionScore = Math.random() * (i % 4 === 0 ? 0.7 : 0.2)
sampleData.push({
id: `memory-${i}`,
score,
updated_at: lastUpdate.toISOString(),
last_retrieved_at: lastRetrievedAt.toISOString(),
retrieval_count: retrievalCount,
joy_score: joyScore.toFixed(2),
aversion_score: aversionScore.toFixed(2),
})
}
for (const item of sampleData) {
await db.execute(`
INSERT INTO emotional_memories_test_table
(id, score, updated_at, last_retrieved_at, retrieval_count, joy_score, aversion_score)
VALUES
('${item.id}', ${item.score}, '${item.updated_at}', '${item.last_retrieved_at}',
${item.retrieval_count}, ${item.joy_score}, ${item.aversion_score})
`)
}
}
export async function simulateEmotionalRetrieval(db, memoryId, simulatedTime, { joyModifier = 0, aversionModifier = 0 }) {
const simulatedTimestamp = simulatedTime.toISOString()
await db.execute(`
UPDATE emotional_memories_test_table
SET last_retrieved_at = '${simulatedTimestamp}',
retrieval_count = retrieval_count + 1,
joy_score = CASE
WHEN joy_score + ${joyModifier} < 0 THEN 0
WHEN joy_score + ${joyModifier} > 1 THEN 1
ELSE joy_score + ${joyModifier}
END,
aversion_score = CASE
WHEN aversion_score + ${aversionModifier} < 0 THEN 0
WHEN aversion_score + ${aversionModifier} > 1 THEN 1
ELSE aversion_score + ${aversionModifier}
END
WHERE id = '${memoryId}'
`)
}
@@ -1,14 +0,0 @@
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
import './styles/themes.css'
const router = createRouter({ routes, history: createWebHashHistory() })
createApp(App)
.use(router)
.mount('#app')
@@ -1,859 +0,0 @@
<script setup lang="ts">
import type { SimulationLinkDatum, SimulationNodeDatum } from 'd3'
import type { DuckDBWasmDrizzleDatabase } from '../../../src'
import * as d3 from 'd3'
// import { eq, inArray } from 'drizzle-orm'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { drizzle } from '../../../src'
import { buildDSN } from '../../../src/dsn'
import * as schema from '../../db/schema'
import { edgeGroups, edgeOwners, edgePets, edgeUsers, nodeGroups, nodePets, nodeUsers } from '../../db/schema'
import migration1 from '../../drizzle/0001_parched_silver_centurion.sql?raw'
const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>()
const results = ref<Record<string, unknown>[]>()
// const schemaResults = ref<Record<string, unknown>[]>()
const isMigrated = ref(false)
interface Node extends SimulationNodeDatum {
id: string
name: string
type: string
}
const graphData = ref<{ nodes: Node[], links: (SimulationLinkDatum<Node> & { type: string })[] }>({ nodes: [], links: [] })
const queryResult = ref(null)
const queryType = ref('simple') // 'simple', 'recursive', 'path'
const selectedStartNode = ref(null)
const selectedEndNode = ref(null)
async function connect() {
isMigrated.value = false
db.value = drizzle(buildDSN({
scheme: 'duckdb-wasm:',
bundles: 'import-url',
logger: true,
}), { schema })
await db.value?.execute('INSTALL vss;')
await db.value?.execute('LOAD vss;')
}
// Manual multi-level traversal: group -> users -> pets
// async function getPetsInGroup(groupId: string) {
// // First get all users in the group
// const groupUsers = await db.value.select({ id: nodeUsers.id })
// .from(nodeUsers)
// .innerJoin(edgeGroups, eq(edgeGroups.target, nodeUsers.id))
// .where(eq(edgeGroups.source, groupId))
// // Then get all pets owned by these users
// const userIds = groupUsers.map(u => u.id)
// return await db.value.select({ pet: nodePets })
// .from(nodePets)
// .innerJoin(edgePets, eq(edgePets.target, nodePets.id))
// .where(inArray(edgePets.source, userIds))
// }
async function migrate() {
await db.value?.execute(migration1)
const group1 = await db.value?.insert(nodeGroups).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'Group 1' }).returning()
const group2 = await db.value?.insert(nodeGroups).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'Group 2' }).returning()
const user1 = await db.value?.insert(nodeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'User 1' }).returning()
const user2 = await db.value?.insert(nodeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'User 2' }).returning()
const user3 = await db.value?.insert(nodeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'User 3' }).returning()
const user4 = await db.value?.insert(nodeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'User 4' }).returning()
const pet1 = await db.value?.insert(nodePets).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'Pet 1' }).returning()
const pet2 = await db.value?.insert(nodePets).values({ id: crypto.randomUUID().replace(/-/g, ''), name: 'Pet 2' }).returning()
// Insert edges
// User to Group edges (One to M)
await db.value?.insert(edgeGroups).values({ id: crypto.randomUUID().replace(/-/g, ''), source: group1[0].id, target: user1[0].id }).returning()
await db.value?.insert(edgeGroups).values({ id: crypto.randomUUID().replace(/-/g, ''), source: group1[0].id, target: user2[0].id }).returning()
await db.value?.insert(edgeGroups).values({ id: crypto.randomUUID().replace(/-/g, ''), source: group2[0].id, target: user3[0].id }).returning()
// Group to User edges (M to M)
await db.value?.insert(edgeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user1[0].id, target: group1[0].id }).returning()
await db.value?.insert(edgeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user2[0].id, target: group2[0].id }).returning()
await db.value?.insert(edgeUsers).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user3[0].id, target: group2[0].id }).returning()
// User to Pet edges (One to M)
await db.value?.insert(edgePets).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user1[0].id, target: pet1[0].id }).returning()
await db.value?.insert(edgePets).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user2[0].id, target: pet2[0].id }).returning()
// Pet to User edges (M to One)
await db.value?.insert(edgeOwners).values({ id: crypto.randomUUID().replace(/-/g, ''), source: pet1[0].id, target: user1[0].id }).returning()
await db.value?.insert(edgeOwners).values({ id: crypto.randomUUID().replace(/-/g, ''), source: pet2[0].id, target: user2[0].id }).returning()
// // Create a more complex relationship - user4 has both pets
await db.value?.insert(edgePets).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user4[0].id, target: pet1[0].id }).returning()
await db.value?.insert(edgePets).values({ id: crypto.randomUUID().replace(/-/g, ''), source: user4[0].id, target: pet2[0].id }).returning()
// First, let's check if our data was inserted correctly
// console.log('Users:', await db.value?.select().from(nodeUsers))
// console.log('Groups:', await db.value?.select().from(nodeGroups))
// console.log('User-Group edges:', await db.value?.select().from(edgeGroups))
// console.log('Group-User edges:', await db.value?.select().from(edgeGroups))
// console.log(await db.value?.execute(`
// WITH RECURSIVE user_network AS (
// -- Base case: start with the given user
// SELECT id, name, 0 AS depth
// FROM node_users
// WHERE id = '${user1[0].id}'
// UNION ALL
// -- Find connections through groups
// SELECT nu.id, nu.name, un.depth + 1
// FROM user_network un
// JOIN edge_users eu ON un.id = eu.source
// JOIN node_groups ng ON eu.target = ng.id
// JOIN edge_groups eg ON ng.id = eg.source
// JOIN node_users nu ON eg.target = nu.id
// WHERE un.depth < 3
// )
// SELECT id, name, depth
// FROM user_network
// ORDER BY depth;`))
isMigrated.value = true
}
// Fetch all nodes and edges for visualization
async function fetchGraphData() {
const users = await db.value?.select().from(nodeUsers)
const groups = await db.value?.select().from(nodeGroups)
const pets = await db.value?.select().from(nodePets)
const userEdges = await db.value?.select().from(edgeUsers)
const groupEdges = await db.value?.select().from(edgeGroups)
const petEdges = await db.value?.select().from(edgePets)
const ownerEdges = await db.value?.select().from(edgeOwners)
// Prepare nodes with different types
const nodes = [
...users.map(u => ({ id: u.id, name: u.name, type: 'user' } satisfies Node)),
...groups.map(g => ({ id: g.id, name: g.name, type: 'group' } satisfies Node)),
...pets.map(p => ({ id: p.id, name: p.name, type: 'pet' } satisfies Node)),
]
// Prepare links with different types
const links = [
...userEdges.map(e => ({ source: e.source, target: e.target, type: 'user-group' })),
...groupEdges.map(e => ({ source: e.source, target: e.target, type: 'group-user' })),
...petEdges.map(e => ({ source: e.source, target: e.target, type: 'user-pet' })),
...ownerEdges.map(e => ({ source: e.source, target: e.target, type: 'pet-owner' })),
]
graphData.value = { nodes, links }
// Set default selected nodes for queries
if (nodes.length > 0) {
selectedStartNode.value = nodes.find(n => n.type === 'user')?.id
selectedEndNode.value = nodes.find(n => n.type === 'user' && n.id !== selectedStartNode.value)?.id
}
}
// Run a simple query to find direct connections
async function runSimpleQuery() {
if (!selectedStartNode.value)
return
const result = await db.value?.execute(`
-- Find direct connections from the selected node
SELECT
n.id, n.name,
CASE
WHEN n.id IN (SELECT id FROM node_users) THEN 'user'
WHEN n.id IN (SELECT id FROM node_groups) THEN 'group'
WHEN n.id IN (SELECT id FROM node_pets) THEN 'pet'
END as type,
e.source, e.target
FROM (
-- User to Group
SELECT eu.source, eu.target, ng.id, ng.name
FROM edge_users eu
JOIN node_groups ng ON eu.target = ng.id
WHERE eu.source = '${selectedStartNode.value}'
UNION ALL
-- User to Pet
SELECT ep.source, ep.target, np.id, np.name
FROM edge_pets ep
JOIN node_pets np ON ep.target = np.id
WHERE ep.source = '${selectedStartNode.value}'
UNION ALL
-- Group to User
SELECT eg.source, eg.target, nu.id, nu.name
FROM edge_groups eg
JOIN node_users nu ON eg.target = nu.id
WHERE eg.source = '${selectedStartNode.value}'
UNION ALL
-- Pet to Owner
SELECT eo.source, eo.target, nu.id, nu.name
FROM edge_owners eo
JOIN node_users nu ON eo.target = nu.id
WHERE eo.source = '${selectedStartNode.value}'
) e
JOIN (
SELECT id, name FROM node_users
UNION ALL
SELECT id, name FROM node_groups
UNION ALL
SELECT id, name FROM node_pets
) n ON e.id = n.id
`)
queryResult.value = result
highlightQueryResults(result)
}
// Run a recursive query to find paths
async function runRecursiveQuery() {
if (!selectedStartNode.value)
return
const result = await db.value?.execute(`
WITH RECURSIVE path_search AS (
-- Base case: start with the given node
SELECT
id,
name,
CASE
WHEN id IN (SELECT id FROM node_users) THEN 'user'
WHEN id IN (SELECT id FROM node_groups) THEN 'group'
WHEN id IN (SELECT id FROM node_pets) THEN 'pet'
END as type,
0 AS depth,
ARRAY[id] AS path
FROM (
SELECT id, name FROM node_users
UNION ALL
SELECT id, name FROM node_groups
UNION ALL
SELECT id, name FROM node_pets
) nodes
WHERE id = '${selectedStartNode.value}'
UNION ALL
-- Recursive case: follow all possible edges
SELECT
target_node.id,
target_node.name,
CASE
WHEN target_node.id IN (SELECT id FROM node_users) THEN 'user'
WHEN target_node.id IN (SELECT id FROM node_groups) THEN 'group'
WHEN target_node.id IN (SELECT id FROM node_pets) THEN 'pet'
END as type,
ps.depth + 1,
ps.path || ARRAY[target_node.id]
FROM path_search ps
JOIN (
-- All possible edges
SELECT source, target FROM edge_users
UNION ALL
SELECT source, target FROM edge_groups
UNION ALL
SELECT source, target FROM edge_pets
UNION ALL
SELECT source, target FROM edge_owners
) edges ON ps.id = edges.source
JOIN (
SELECT id, name FROM node_users
UNION ALL
SELECT id, name FROM node_groups
UNION ALL
SELECT id, name FROM node_pets
) target_node ON edges.target = target_node.id
WHERE ps.depth < 3
AND NOT target_node.id = ANY(ps.path) -- Avoid cycles
)
SELECT id, name, type, depth, path
FROM path_search
WHERE depth > 0 -- Exclude the starting node
ORDER BY depth, name
`)
queryResult.value = result
highlightQueryResults(result)
}
// Run a path finding query between two nodes
async function runPathQuery() {
if (!selectedStartNode.value || !selectedEndNode.value)
return
const result = await db.value?.execute(`
WITH RECURSIVE path_search AS (
-- Base case: start with the given node
SELECT
id,
name,
CASE
WHEN id IN (SELECT id FROM node_users) THEN 'user'
WHEN id IN (SELECT id FROM node_groups) THEN 'group'
WHEN id IN (SELECT id FROM node_pets) THEN 'pet'
END as type,
0 AS depth,
ARRAY[id] AS path,
id = '${selectedEndNode.value}' AS found
FROM (
SELECT id, name FROM node_users
UNION ALL
SELECT id, name FROM node_groups
UNION ALL
SELECT id, name FROM node_pets
) nodes
WHERE id = '${selectedStartNode.value}'
UNION ALL
-- Recursive case: follow all possible edges
SELECT
target_node.id,
target_node.name,
CASE
WHEN target_node.id IN (SELECT id FROM node_users) THEN 'user'
WHEN target_node.id IN (SELECT id FROM node_groups) THEN 'group'
WHEN target_node.id IN (SELECT id FROM node_pets) THEN 'pet'
END as type,
ps.depth + 1,
ps.path || ARRAY[target_node.id],
target_node.id = '${selectedEndNode.value}' AS found
FROM path_search ps
JOIN (
-- All possible edges
SELECT source, target FROM edge_users
UNION ALL
SELECT source, target FROM edge_groups
UNION ALL
SELECT source, target FROM edge_pets
UNION ALL
SELECT source, target FROM edge_owners
) edges ON ps.id = edges.source
JOIN (
SELECT id, name FROM node_users
UNION ALL
SELECT id, name FROM node_groups
UNION ALL
SELECT id, name FROM node_pets
) target_node ON edges.target = target_node.id
WHERE ps.depth < 5
AND NOT target_node.id = ANY(ps.path) -- Avoid cycles
AND NOT ps.found -- Stop once we've found the target
)
SELECT id, name, type, depth, path
FROM path_search
WHERE found = true
ORDER BY depth
LIMIT 1
`)
queryResult.value = result
highlightQueryResults(result)
}
// Highlight nodes and edges based on query results
function highlightQueryResults(results) {
// Reset all highlights
d3.selectAll('.node').classed('highlighted', false).classed('in-path', false)
d3.selectAll('.link').classed('highlighted', false).classed('in-path', false)
if (!results || !results.length)
return
// For path queries, highlight the entire path
if (results[0].path) {
const pathIds = new Set()
const pathLinks = new Set()
results.forEach((row) => {
if (row.path && row.path.length > 1) {
// Add all nodes in the path
row.path.forEach(id => pathIds.add(id))
// Add all links in the path
for (let i = 0; i < row.path.length - 1; i++) {
pathLinks.add(`${row.path[i]}-${row.path[i + 1]}`)
}
}
})
// Highlight nodes in the path
d3.selectAll('.node')
.classed('in-path', (d: { id: string }) => pathIds.has(d.id))
// Highlight links in the path
d3.selectAll('.link')
.classed('in-path', (d: { source: { id: string }, target: { id: string } }) => {
return pathLinks.has(`${d.source.id}-${d.target.id}`)
|| pathLinks.has(`${d.target.id}-${d.source.id}`)
})
}
// For simple queries, just highlight the direct connections
else {
const nodeIds = new Set(results.map(r => r.id))
// Highlight nodes
d3.selectAll('.node')
.classed('highlighted', (d: { id: string }) => nodeIds.has(d.id))
// Highlight links
d3.selectAll('.link')
.classed('highlighted', (d: { source: { id: string }, target: { id: string } }) => {
return (d.source.id === selectedStartNode.value && nodeIds.has(d.target.id))
|| (d.target.id === selectedStartNode.value && nodeIds.has(d.source.id))
})
}
}
// Run the appropriate query based on the selected type
function runQuery() {
switch (queryType.value) {
case 'simple':
runSimpleQuery()
break
case 'recursive':
runRecursiveQuery()
break
case 'path':
runPathQuery()
break
}
}
// Create the D3 force-directed graph
function createGraph() {
const width = 800
const height = 600
// Clear previous graph
d3.select('#graph-container').selectAll('*').remove()
// Create tooltip div
const tooltip = d3.select('#graph-container')
.append('div')
.attr('class', 'tooltip w-full bg-neutral-100 dark:bg-neutral-800')
.style('opacity', 0)
.style('position', 'absolute')
.style('padding', '8px')
.style('border-radius', '4px')
.style('pointer-events', 'none')
.style('z-index', 10)
.style('font-size', '12px')
.style('max-width', '200px')
const svg = d3.select('#graph-container')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height])
// Define arrow markers for links
svg.append('defs').selectAll('marker').data(['user-group', 'group-user', 'user-pet', 'pet-owner']).join('marker').attr('id', d => `arrow-${d}`).attr('viewBox', '0 -5 10 10').attr('refX', 15).attr('refY', 0).attr('markerWidth', 6).attr('markerHeight', 6).attr('orient', 'auto').append('path').attr('fill', (d) => {
switch (d) {
case 'user-group': return '#4CAF50'
case 'group-user': return '#2196F3'
case 'user-pet': return '#FF9800'
case 'pet-owner': return '#9C27B0'
default: return '#999'
}
}).attr('d', 'M0,-5L10,0L0,5')
// Create the simulation
const simulation = d3.forceSimulation(graphData.value.nodes)
.force('link', d3.forceLink(graphData.value.links).id(d => ('id' in d ? d.id : d) as string).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(40))
// Create links
const link = svg.append('g')
.selectAll('line')
.data(graphData.value.links)
.join('line')
.attr('class', 'link')
.attr('stroke', (d) => {
switch (d.type) {
case 'user-group': return '#4CAF50'
case 'group-user': return '#2196F3'
case 'user-pet': return '#FF9800'
case 'pet-owner': return '#9C27B0'
default: return '#999'
}
})
.attr('stroke-width', 2)
.attr('marker-end', d => `url(#arrow-${d.type})`)
.on('mouseover', function (event, d) {
// Highlight the link
d3.select(this).attr('stroke-width', 4)
// Show tooltip with edge information
tooltip.transition()
.duration(100)
.style('opacity', 0.9)
.ease()
let relationshipText = ''
switch (d.type) {
case 'user-group':
relationshipText = 'User belongs to Group'
break
case 'group-user':
relationshipText = 'Group contains User'
break
case 'user-pet':
relationshipText = 'User owns Pet'
break
case 'pet-owner':
relationshipText = 'Pet owned by User'
break
}
// Find source and target node names
const sourceNode = graphData.value.nodes.find(n => n.id === (d.source as Node).id || n.id === d.source)
const targetNode = graphData.value.nodes.find(n => n.id === (d.target as Node).id || n.id === d.target)
tooltip.html(`
<div class="font-bold">${relationshipText}</div>
<div>From: ${sourceNode?.name || 'Unknown'}</div>
<div>To: ${targetNode?.name || 'Unknown'}</div>
`)
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 28}px`)
})
.on('mouseout', function () {
// Reset link thickness
d3.select(this).attr('stroke-width', 2)
// Hide tooltip
tooltip.transition()
.duration(100)
.style('opacity', 0)
.ease()
})
// Create node groups
const node = svg.append('g')
.selectAll('.node')
.data(graphData.value.nodes)
.join('g')
.attr('class', 'node')
.call(d3.drag<SVGCircleElement, Node>()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended))
.on('click', (event, d) => {
// Toggle selection of nodes for queries
if (event.ctrlKey || event.metaKey) {
selectedEndNode.value = d.id
}
else {
selectedStartNode.value = d.id
}
runQuery()
})
.on('mouseover', function (event, d) {
// Show tooltip with node information
tooltip.transition()
.duration(100)
.style('opacity', 0.9)
.ease()
// Show if this is the selected start or end node
let selectionStatus = ''
if (d.id === selectedStartNode.value) {
selectionStatus = '<div class="text-green-400">Start Node</div>'
}
else if (d.id === selectedEndNode.value) {
selectionStatus = '<div class="text-red-400">End Node</div>'
}
tooltip.html(`
<div class="font-bold">${d.name}</div>
<div class="text-xs dark:text-neutral-300 text-neutral-500">Type: ${d.type}</div>
<div class="text-xs dark:text-neutral-300 text-neutral-500">ID: ${d.id}</div>
${selectionStatus}
`)
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 28}px`)
// Highlight the node
d3.select(this).select('circle').transition().duration(100).attr('r', 25).ease()
})
.on('mouseout', function () {
// Hide tooltip
tooltip.transition()
.duration(100)
.style('opacity', 0)
.ease()
// Reset node size
d3.select(this).select('circle').transition().duration(100).attr('r', 20).ease()
})
// Add circles to nodes
node.append('circle')
.attr('r', 20)
.attr('fill', (d) => {
switch (d.type) {
case 'user': return '#E91E63'
case 'group': return '#3F51B5'
case 'pet': return '#FFC107'
default: return '#999'
}
})
// Add small icons or initials inside nodes instead of full text
node.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('fill', 'white')
.attr('font-size', '10px')
.text((d) => {
switch (d.type) {
case 'user': return 'U'
case 'group': return 'G'
case 'pet': return 'P'
default: return ''
}
})
// Update positions on each tick
simulation.on('tick', () => {
link
.attr('x1', d => (d.source as SimulationNodeDatum).x)
.attr('y1', d => (d.source as SimulationNodeDatum).y)
.attr('x2', d => (d.target as SimulationNodeDatum).x)
.attr('y2', d => (d.target as SimulationNodeDatum).y)
node.attr('transform', d => `translate(${d.x},${d.y})`)
})
// Drag functions
function dragstarted(event) {
if (!event.active)
simulation.alphaTarget(0.3).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
}
function dragged(event) {
event.subject.fx = event.x
event.subject.fy = event.y
}
function dragended(event) {
if (!event.active)
simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
}
}
// Watch for changes in graph data and recreate the visualization
watch(graphData, () => {
if (graphData.value.nodes.length > 0) {
createGraph()
}
})
onMounted(async () => {
await connect()
await migrate()
results.value = await db.value?.execute('SHOW TABLES;')
// const usersResults = await db.value?.select().from(users)
// schemaResults.value = usersResults
// console.log(results.value)
await fetchGraphData()
isMigrated.value = true
})
onUnmounted(() => {
db.value?.$client.then(client => client.close())
})
</script>
<template>
<div class="p-4">
<div v-if="!isMigrated" class="py-8 text-center">
<div class="mx-auto h-8 w-8 animate-spin border-4 border-blue-500 border-t-transparent rounded-full" />
<p class="mt-4">
Initializing database...
</p>
</div>
<div v-else class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<!-- Graph Visualization -->
<div class="rounded-lg p-4 shadow lg:col-span-2">
<h2 class="mb-2 text-lg font-semibold">
Visualize
</h2>
<div id="graph-container" class="h-[600px] w-full" />
<div class="mt-4 flex flex-wrap gap-2">
<div class="flex items-center">
<div class="mr-2 h-4 w-4 rounded-full bg-[#E91E63]" />
<span>User</span>
</div>
<div class="flex items-center">
<div class="mr-2 h-4 w-4 rounded-full bg-[#3F51B5]" />
<span>Group</span>
</div>
<div class="flex items-center">
<div class="mr-2 h-4 w-4 rounded-full bg-[#FFC107]" />
<span>Pet</span>
</div>
</div>
</div>
<!-- Controls and Results -->
<div class="space-y-4">
<!-- Query Controls -->
<div class="rounded-lg p-4 shadow">
<h2 class="mb-2 text-lg font-semibold">
Query Controls
</h2>
<div class="mb-4">
<label class="mb-1 block text-sm font-medium">Query Type</label>
<select v-model="queryType" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
<option value="recursive">
Recursive (All Paths)
</option>
<option value="path">
Find Path Between Nodes
</option>
<option value="simple">
Direct Connections
</option>
</select>
</div>
<div class="mb-4">
<label class="mb-1 block text-sm font-medium">Start Node</label>
<select v-model="selectedStartNode" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
<option v-for="node in graphData.nodes" :key="`start-${node.id}`" :value="node.id">
{{ node.name }} ({{ node.type }})
</option>
</select>
</div>
<div v-if="queryType === 'path'" class="mb-4">
<label class="mb-1 block text-sm font-medium">End Node</label>
<select v-model="selectedEndNode" class="w-full rounded border-none bg-neutral-100 p-2 outline-none dark:bg-neutral-800">
<option v-for="node in graphData.nodes" :key="`end-${node.id}`" :value="node.id">
{{ node.name }} ({{ node.type }})
</option>
</select>
</div>
<button class="w-full rounded bg-blue-100 px-4 py-2 dark:bg-blue-900" @click="runQuery">
Run Query
</button>
<div class="mt-2 text-sm text-neutral-500">
<p>Click a node to select it as start node.</p>
<p>Ctrl+Click to select as end node.</p>
</div>
</div>
<!-- Query Results -->
<div v-if="queryResult" class="rounded-lg p-4 shadow">
<h2 class="mb-2 text-lg font-semibold">
Query Results
</h2>
<div class="max-h-[300px] overflow-auto">
<table class="min-w-full">
<thead mb-2>
<tr>
<th
class="border-b px-2 py-1 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase"
>
Name
</th>
<th
class="border-b px-2 py-1 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase"
>
Type
</th>
<th
class="border-b px-2 py-1 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase"
>
Depth
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in queryResult" :key="i" class="hover:bg-neutral-50 dark:hover:bg-neutral-800"
transition="all duration-250 ease-in-out" of-hidden
>
<td class="whitespace-nowrap px-2 py-1 text-sm" rounded-l-lg>
{{ row.name }}
</td>
<td class="whitespace-nowrap px-2 py-1 text-sm">
{{ row.type }}
</td>
<td class="whitespace-nowrap px-2 py-1 text-sm" rounded-r-lg>
{{ row.depth }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.node circle {
stroke: #fff;
stroke-width: 2px;
transition: all 0.2s ease;
}
.node.highlighted circle {
stroke: #ff0;
stroke-width: 3px;
}
.node.in-path circle {
stroke: #ff0;
stroke-width: 3px;
filter: drop-shadow(0 0 5px rgba(255, 255, 0, 0.7));
}
.link {
stroke-opacity: 0.6;
transition: all 0.2s ease;
}
.link.highlighted {
stroke-opacity: 1;
stroke-width: 3px;
}
.link.in-path {
stroke-opacity: 1;
stroke-width: 3px;
filter: drop-shadow(0 0 3px rgba(255, 255, 0, 0.5));
}
.tooltip {
transition: opacity 0.3s ease;
}
</style>
@@ -1,267 +0,0 @@
<script setup lang="ts">
import type { DuckDBWasmDrizzleDatabase } from '../../../src'
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
import { DBStorageType } from '@proj-airi/duckdb-wasm'
import { serialize } from 'superjson'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { drizzle } from '../../../src'
import { buildDSN } from '../../../src/dsn'
import * as schema from '../../db/schema'
import { users } from '../../db/schema'
import migration1 from '../../drizzle/0000_cute_kulan_gath.sql?raw'
const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>()
const results = ref<Record<string, unknown>[]>()
const schemaResults = ref<Record<string, unknown>[]>()
const isMigrated = ref(false)
const storage = ref<DBStorageType>()
const path = ref('test.db')
const logger = ref(true)
const readOnly = ref(false)
const dsn = computed(() => {
return buildDSN({
scheme: 'duckdb-wasm:',
bundles: 'import-url',
logger: logger.value,
...storage.value === DBStorageType.ORIGIN_PRIVATE_FS && {
storage: {
type: storage.value,
path: path.value,
accessMode: readOnly.value ? DuckDBAccessMode.READ_ONLY : DuckDBAccessMode.READ_WRITE,
},
},
})
})
const query = ref(`SELECT * FROM 'users'`)
async function connect() {
isMigrated.value = false
db.value = drizzle(dsn.value, { schema })
await db.value?.execute('INSTALL vss;')
await db.value?.execute('LOAD vss;')
}
async function migrate() {
await db.value?.execute(migration1)
await db.value?.insert(users).values({
id: '9449af72-faad-4c97-8a45-69f9f1ca1b05',
decimal: '1.23456',
numeric: '1.23456',
real: 1.23456,
double: 1.23456,
interval: '365 day',
})
isMigrated.value = true
}
async function insert() {
await db.value?.insert(users).values({
id: crypto.randomUUID().replace(/-/g, ''),
decimal: '1.23456',
numeric: '1.23456',
real: 1.23456,
double: 1.23456,
interval: '365 day',
})
}
async function reconnect() {
const client = await db.value?.$client
await client?.close()
await connect()
}
async function execute() {
results.value = await db.value?.execute(query.value)
}
async function executeORM() {
schemaResults.value = await db.value?.select().from(users)
}
async function shallowListOPFS() {
const opfsRoot = await navigator.storage.getDirectory()
const files: string[] = []
for await (const name of opfsRoot.keys()) {
files.push(name)
}
// eslint-disable-next-line no-console
console.log(['Files in OPFS:', ...files].join('\n'))
}
async function wipeOPFS() {
await db.value?.$client.then(client => client.close())
const opfsRoot = await navigator.storage.getDirectory()
const promises: Promise<void>[] = []
for await (const name of opfsRoot.keys()) {
promises.push(opfsRoot.removeEntry(name, { recursive: true }).then(() => {
// eslint-disable-next-line no-console
console.info(`File removed from OPFS: "${name}"`)
}))
}
await Promise.all(promises)
}
onMounted(async () => {
await connect()
await migrate()
results.value = await db.value?.execute(query.value)
const usersResults = await db.value?.select().from(users)
schemaResults.value = usersResults
})
onUnmounted(() => {
db.value?.$client.then(client => client.close())
})
</script>
<template>
<div flex flex-col gap-2>
<h2 text-xl>
Storage
</h2>
<div flex flex-row gap-2>
<div flex flex-row gap-2>
<input id="in-memory" v-model="storage" type="radio" :value="undefined">
<label for="in-memory">In-Memory</label>
</div>
<div flex flex-row gap-2>
<input id="opfs" v-model="storage" type="radio" :value="DBStorageType.ORIGIN_PRIVATE_FS">
<label for="opfs">Origin Private FS</label>
</div>
</div>
</div>
<div grid grid-cols-3 gap-2>
<div flex flex-col gap-2>
<h2 text-xl>
Logger
</h2>
<div flex flex-row gap-2>
<input id="logger" v-model="logger" type="checkbox">
<label for="logger">Enable</label>
</div>
</div>
<div flex flex-col gap-2>
<h2 text-xl>
Read-only
</h2>
<div flex flex-row gap-2>
<input id="readOnly" v-model="readOnly" type="checkbox">
<label for="readOnly">Read-only (DB file creation will fail)</label>
</div>
</div>
</div>
<div v-if="storage === DBStorageType.ORIGIN_PRIVATE_FS" flex flex-col gap-2>
<h2 text-xl>
Path
</h2>
<div flex flex-col gap-1>
<input v-model="path" type="text" w-full rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
<div text-sm>
<ul list-disc-inside>
<li>
Leading slash is optional ("/path/to/database.db" is equivalent to "path/to/database.db")
</li>
<li>Empty path is INVALID</li>
</ul>
</div>
</div>
</div>
<div flex flex-col gap-2>
<h2 text-xl>
DSN (read-only)
</h2>
<div>
<input v-model="dsn" readonly type="text" w-full rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
</div>
</div>
<div flex flex-row justify-between gap-2>
<div flex flex-row gap-2>
<button rounded-lg bg="cyan-100 dark:cyan-900" px-4 py-2 @click="reconnect">
Reconnect
</button>
<button
rounded-lg bg="blue-100 dark:blue-900" px-4 py-2 :class="{ 'cursor-not-allowed': isMigrated }"
:disabled="isMigrated" @click="migrate"
>
{{ isMigrated ? 'Already migrated 🥳' : 'Migrate' }}
</button>
<button rounded-lg bg="violet-100 dark:violet-900" px-4 py-2 @click="insert">
Insert
</button>
</div>
<div flex flex-row gap-2>
<button rounded-lg bg="cyan-100 dark:cyan-900" px-4 py-2 @click="shallowListOPFS">
List OPFS (See console)
</button>
<button rounded-lg bg="violet-100 dark:violet-900" px-4 py-2 @click="wipeOPFS">
Wipe OPFS
</button>
</div>
</div>
<div grid grid-cols-2 gap-2>
<div flex flex-col gap-2>
<h2 text-xl>
Executing
</h2>
<div>
<textarea v-model="query" h-full w-full rounded-lg bg="neutral-100 dark:neutral-800" p-4 font-mono />
</div>
<div flex flex-row gap-2>
<button rounded-lg bg="blue-100 dark:blue-900" px-4 py-2 @click="execute">
Execute
</button>
</div>
<div flex flex-col gap-2>
<h2 text-xl>
Results
</h2>
<div whitespace-pre-wrap p-4 font-mono>
{{ JSON.stringify(serialize(results).json, null, 2) }}
</div>
</div>
</div>
<div>
<div flex flex-col gap-2>
<h2 text-xl>
Executing (ORM, read-only)
</h2>
<div>
<pre whitespace-pre-wrap rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
await db
.insert(users)
.values({
id: crypto.randomUUID().replace(/-/g, ''),
decimal: '1.23456',
numeric: '1.23456',
real: 1.23456,
double: 1.23456,
interval: '365 day',
})
await db.select().from(users)
</pre>
</div>
<div flex flex-row gap-2>
<button rounded-lg bg="blue-100 dark:blue-900" px-4 py-2 @click="executeORM">
Execute
</button>
</div>
</div>
<div flex flex-col gap-2>
<h2 text-xl>
Schema Results
</h2>
<div whitespace-pre-wrap p-4 font-mono>
{{ JSON.stringify(serialize(schemaResults).json, null, 2) }}
</div>
</div>
</div>
</div>
</template>
@@ -1,345 +0,0 @@
<script setup lang="ts">
import type { MemoryItem } from '../types/memory/memory-decay'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import MemoryModelSettings from '../components/Memory/DecayModelSettings.vue'
import TimeControls from '../components/Memory/DecayTimeSettings.vue'
import MemoryDetails from '../components/Memory/RecordDetail.vue'
import MemoryChart from '../components/Memory/VisualizeChart.vue'
import MemoryTable from '../components/Memory/VisualizeTable.vue'
import {
connectToDatabase,
createSchema,
generateDecayQuery,
loadSampleData,
simulateRetrieval,
} from '../composables/memory/memory-decay-db'
// Database references
const db = ref(null)
const isMigrated = ref(false)
// Data and query results
const rawData = ref<MemoryItem[]>([])
const decayedResults = ref<MemoryItem[]>([])
// Parameters for decay function
const decayRate = ref(0.0990)
const timeUnit = ref('days')
const maxDaysToShow = ref(30)
const selectedStoryId = ref(null)
// Time acceleration parameters
const timeMultiplier = ref(60 * 60 * 24) // Default: 1 day/second
const isTimeAccelerated = ref(false)
const simulatedTimeOffset = ref(0)
const lastTickTime = ref(Date.now())
// Advanced memory model parameters
const retrievalBoost = ref(0.25)
const retrievalDecaySlowdown = ref(0.8)
const longTermMemoryEnabled = ref(true)
const longTermMemoryThreshold = ref(5)
const longTermMemoryStability = ref(0.3)
const longTermMemoryVisualize = ref(true)
// UI elements
const showInfoPanel = ref(false)
// Selected memory
const selectedMemory = computed(() => {
if (!selectedStoryId.value || !decayedResults.value.length)
return null
return decayedResults.value.find(s => s.id === selectedStoryId.value)
})
// Time unit in seconds
const timeUnitInSeconds = computed(() => {
switch (timeUnit.value) {
case 'hours': return 60 * 60
case 'days': return 24 * 60 * 60
case 'weeks': return 7 * 24 * 60 * 60
case 'months': return 30 * 24 * 60 * 60
default: return 24 * 60 * 60
}
})
// Current simulated time
const currentSimulatedTime = computed(() => {
const now = new Date()
now.setSeconds(now.getSeconds() + simulatedTimeOffset.value)
return now
})
// Initialize database and load data
async function initialize() {
db.value = await connectToDatabase()
await createSchema(db.value)
await loadSampleData(db.value)
isMigrated.value = true
await loadData()
}
// Load data from the database
async function loadData() {
rawData.value = await db.value?.execute(`
SELECT id, score, updated_at, last_retrieved_at, retrieval_count,
CAST(updated_at AS VARCHAR) as updated_at_str,
CAST(last_retrieved_at AS VARCHAR) as last_retrieved_str
FROM memories_decay_test_table
ORDER BY score DESC
`) || []
await runDecayQuery()
}
const decayQuery = ref('')
watch([simulatedTimeOffset, decayRate, timeUnit, longTermMemoryEnabled, longTermMemoryThreshold, longTermMemoryStability, retrievalBoost, retrievalDecaySlowdown], async () => {
const query = await generateDecayQuery(db.value, {
simulatedTimeOffset: simulatedTimeOffset.value,
decayRate: decayRate.value,
timeUnitInSeconds: timeUnitInSeconds.value,
longTermMemoryEnabled: longTermMemoryEnabled.value,
longTermMemoryThreshold: longTermMemoryThreshold.value,
longTermMemoryStability: longTermMemoryStability.value,
retrievalBoost: retrievalBoost.value,
retrievalDecaySlowdown: retrievalDecaySlowdown.value,
})
decayQuery.value = query
})
// Run the decay query
async function runDecayQuery() {
if (!decayQuery.value)
return
decayedResults.value = await db.value?.execute(decayQuery.value) || []
// Initialize selectedStoryId if not set or update if selection changed
if (!selectedStoryId.value && decayedResults.value.length) {
selectedStoryId.value = decayedResults.value[0].id
}
}
// Handle simulated retrieval
async function handleRetrieval(storyId) {
await simulateRetrieval(db.value, storyId, currentSimulatedTime.value)
await loadData()
}
// Handle time jump
async function handleTimeJump({ amount, unit }) {
let secondsToAdd = 0
switch (unit) {
case 'hour':
secondsToAdd = 60 * 60
break
case 'day':
secondsToAdd = 24 * 60 * 60
break
case 'week':
secondsToAdd = 7 * 24 * 60 * 60
break
case 'month':
secondsToAdd = 30 * 24 * 60 * 60
break
}
simulatedTimeOffset.value += amount * secondsToAdd
await runDecayQuery()
}
// Toggle info panel
function toggleInfoPanel() {
showInfoPanel.value = !showInfoPanel.value
}
// Time acceleration handling
function tickTime() {
if (!isTimeAccelerated.value)
return
const now = Date.now()
const elapsedMs = now - lastTickTime.value
simulatedTimeOffset.value += (elapsedMs / 1000) * timeMultiplier.value
lastTickTime.value = now
runDecayQuery()
requestAnimationFrame(tickTime)
}
// Watch for parameter changes
watch([
decayRate,
timeUnit,
maxDaysToShow,
retrievalBoost,
retrievalDecaySlowdown,
longTermMemoryEnabled,
longTermMemoryThreshold,
longTermMemoryStability,
longTermMemoryVisualize,
], async () => {
if (isMigrated.value) {
await runDecayQuery()
}
})
// Watch time acceleration state
watch(isTimeAccelerated, (newValue) => {
if (newValue) {
lastTickTime.value = Date.now()
tickTime()
}
})
// Watch time multiplier changes
watch(timeMultiplier, () => {
if (isTimeAccelerated.value) {
lastTickTime.value = Date.now()
}
})
// Lifecycle hooks
onMounted(async () => {
await initialize()
isTimeAccelerated.value = true
})
onUnmounted(() => {
isTimeAccelerated.value = false
db.value?.$client.then(client => client.close())
})
</script>
<template>
<div class="mx-auto max-w-7xl">
<!-- Loading state -->
<div v-if="!isMigrated" class="py-8 text-center">
<div class="mx-auto h-8 w-8 animate-spin border-4 border-blue-500 border-t-transparent rounded-full" />
<p class="mt-4">
Initializing database and sample data...
</p>
</div>
<div v-else>
<header class="mb-6">
<h1 class="mb-2 text-2xl font-bold">
Memory Decay & Retention Simulator
</h1>
<div class="flex items-center justify-between">
<p class="max-w-2xl text-neutral-600 dark:text-neutral-300">
Visualize how memories decay over time and how repeated retrievals create long-term memory
<button class="ml-2 text-blue-500 hover:underline" @click="toggleInfoPanel">
{{ showInfoPanel ? 'Hide info' : 'Learn more' }}
</button>
</p>
</div>
</header>
<!-- Info Panel -->
<div v-if="showInfoPanel" class="mb-6 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-700/20">
<h2 class="mb-2 text-lg font-semibold">
About Memory Decay and Long-Term Memory Formation
</h2>
<p class="mb-2">
This simulator models both the forgetting curve and how memories become more stable with repeated retrievals:
</p>
<div class="my-3 rounded bg-white p-3 text-center font-mono dark:bg-neutral-800">
score * exp(-decay_rate * time_elapsed / time_unit * (1 - ltm_factor)) * (1 + retrieval_boost)
</div>
<p class="font-medium">
The simulator models three key memory phenomena:
</p>
<ol class="mt-2 list-decimal pl-5 space-y-1">
<li>
<strong>Exponential decay</strong>: Memories naturally fade over time following Ebbinghaus' forgetting curve
</li>
<li>
<strong>Retrieval practice effect</strong>: Each retrieval boosts the memory strength temporarily
</li>
<li>
<strong>Long-term memory formation</strong>: After sufficient retrievals, memories become increasingly stable
and resistant to decay, eventually becoming "permanent"
</li>
</ol>
<p class="mt-3">
This reflects how the brain forms long-term memories through a process of neural consolidation,
where repeated activation of neural pathways leads to structural changes that stabilize memories.
The spaced repetition study method leverages this effect for efficient learning.
</p>
</div>
<!-- Time Controls -->
<TimeControls
v-model:simulated-time-offset="simulatedTimeOffset"
v-model:is-time-accelerated="isTimeAccelerated"
v-model:time-multiplier="timeMultiplier"
class="mb-4"
@time-jump="handleTimeJump"
/>
<!-- Interactive Chart Section -->
<div class="grid grid-cols-1 mb-4 gap-4 lg:grid-cols-3">
<!-- Chart -->
<MemoryChart
:memory-data="decayedResults"
:selected-story-id="selectedStoryId"
:decay-rate="decayRate"
:max-days-to-show="maxDaysToShow"
:long-term-memory-enabled="longTermMemoryEnabled"
:long-term-memory-threshold="longTermMemoryThreshold"
:long-term-memory-visualize="longTermMemoryVisualize"
:retrieval-boost="retrievalBoost"
:retrieval-decay-slowdown="retrievalDecaySlowdown"
class="lg:col-span-2"
/>
<!-- Selected Memory Details -->
<MemoryDetails
v-if="selectedMemory"
:memory="selectedMemory"
:long-term-memory-enabled="longTermMemoryEnabled"
:long-term-memory-threshold="longTermMemoryThreshold"
@retrieve="handleRetrieval"
/>
</div>
<!-- Memory Model Settings -->
<MemoryModelSettings
v-model:long-term-memory-enabled="longTermMemoryEnabled"
v-model:long-term-memory-threshold="longTermMemoryThreshold"
v-model:long-term-memory-stability="longTermMemoryStability"
v-model:long-term-memory-visualize="longTermMemoryVisualize"
v-model:retrieval-boost="retrievalBoost"
v-model:retrieval-decay-slowdown="retrievalDecaySlowdown"
v-model:decay-rate="decayRate"
v-model:time-unit="timeUnit"
v-model:max-days-to-show="maxDaysToShow"
/>
<!-- Memory Table -->
<MemoryTable
:memories="decayedResults"
:selected-id="selectedStoryId"
:long-term-memory-enabled="longTermMemoryEnabled"
:long-term-memory-threshold="longTermMemoryThreshold"
class="mb-4"
@select="selectedStoryId = $event"
@retrieve="handleRetrieval"
/>
<!-- SQL Query Preview -->
<div class="max-w-full rounded-xl bg-neutral-50 dark:bg-neutral-800">
<div class="overflow-x-scroll rounded bg-neutral-800 text-sm text-neutral-200" font-mono>
<pre whitespace-pre-wrap>
<code>{{ decayQuery }}</code>
</pre>
</div>
</div>
</div>
</div>
</template>
@@ -1,614 +0,0 @@
<script setup lang="ts">
import type { EmotionalMemoryItem } from '../types/memory/emotional-memory'
import { useDebounceFn } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
// Import the components
import EmotionalMemoryChart from '../components/Memory/EmotionalMemoryChart.vue'
import EmotionalMemoryDetail from '../components/Memory/EmotionalMemoryDetail.vue'
import EmotionalSettings from '../components/Memory/EmotionalSettings.vue'
import MemoryRetrievalHeatmap from '../components/Memory/MemoryRetrievalHeatmap.vue'
import Range from '../components/Range.vue'
import {
connectToDatabase,
createEmotionalSchema,
generateEmotionalDecayQuery as generateEmotionalQuery,
loadEmotionalSampleData,
simulateEmotionalRetrieval,
} from '../composables/memory/memory-decay-db'
// Database references
const db = ref(null)
const isMigrated = ref(false)
// Data and query results
const processedResults = ref<EmotionalMemoryItem[]>([])
const selectedMemoryId = ref(null)
// Memory model parameters
const decayRate = ref(0.0990)
const timeUnit = ref('days')
const maxDaysToProject = ref(2000)
// Emotional memory parameters
const joyBoostFactor = ref(1.5)
const joyDecaySteepness = ref(3.0)
const aversionSpikeFactor = ref(2.0)
const aversionStability = ref(1.2)
const randomRecallProbability = ref(0.05)
const flashbackIntensity = ref(2.0)
// Time simulation parameters
const timeMultiplier = ref(1) // Default: 1 day/second
const isTimeAccelerated = ref(false)
const simulatedTimeOffset = ref(0)
const lastTickTime = ref(Date.now())
// Memory access thresholds
const longTermThreshold = ref(5)
const muscleMemoryThreshold = ref(40)
// UI controls
const showAdvancedSettings = ref(false)
// Selected memory
const selectedMemory = computed(() => {
if (!selectedMemoryId.value || !processedResults.value.length)
return null
return processedResults.value.find(m => m.id === selectedMemoryId.value)
})
// Time unit in seconds
const timeUnitInSeconds = computed(() => {
switch (timeUnit.value) {
case 'hours': return 60 * 60
case 'days': return 24 * 60 * 60
case 'weeks': return 7 * 24 * 60 * 60
case 'months': return 30 * 24 * 60 * 60
default: return 24 * 60 * 60
}
})
// Current simulated time
const currentSimulatedTime = computed(() => {
const now = new Date()
now.setSeconds(now.getSeconds() + simulatedTimeOffset.value)
return now
})
// Add a new ref for heatmap configuration
const heatmapTimeRange = ref(30) // Show last 30 days by default
// Add this after the other refs
const memoryHistory = ref(new Map())
// Maximum number of history points to track per memory
const historyLength = 20
// Initialize database and load data
async function initialize() {
db.value = await connectToDatabase()
await createEmotionalSchema(db.value)
await loadEmotionalSampleData(db.value)
isMigrated.value = true
await runDecayQuery()
}
// Emotional decay query
const emotionalDecayQuery = ref('')
// Watch for parameter changes
watch([
simulatedTimeOffset,
decayRate,
timeUnit,
longTermThreshold,
muscleMemoryThreshold,
joyBoostFactor,
joyDecaySteepness,
aversionSpikeFactor,
aversionStability,
randomRecallProbability,
flashbackIntensity,
], async () => {
const query = await generateEmotionalQuery(db.value, {
simulatedTimeOffset: simulatedTimeOffset.value,
decayRate: decayRate.value,
timeUnitInSeconds: timeUnitInSeconds.value,
longTermMemoryEnabled: true,
longTermMemoryThreshold: longTermThreshold.value,
longTermMemoryStability: 0.3,
retrievalBoost: 0.25,
retrievalDecaySlowdown: 0.8,
joyBoostFactor: joyBoostFactor.value,
joyDecaySteepness: joyDecaySteepness.value,
aversionSpikeFactor: aversionSpikeFactor.value,
aversionStability: aversionStability.value,
randomRecallProbability: randomRecallProbability.value,
flashbackIntensity: flashbackIntensity.value,
})
emotionalDecayQuery.value = query
})
// Run the decay query
async function runDecayQuery() {
if (!emotionalDecayQuery.value)
return
const queryResults = await db.value?.execute(emotionalDecayQuery.value) || []
// Update memory history with new data points
for (const memory of queryResults) {
// Initialize history array if doesn't exist
if (!memoryHistory.value.has(memory.id)) {
memoryHistory.value.set(memory.id, [])
}
const history = memoryHistory.value.get(memory.id)
// Add current state to history with timestamp
history.push({
timestamp: Date.now(),
simulatedTime: simulatedTimeOffset.value,
score: memory.decayed_score,
joy: memory.joy_score,
aversion: memory.aversion_score,
retrievalCount: memory.retrieval_count,
})
// Trim history to maintain fixed length
if (history.length > historyLength) {
history.shift()
}
}
processedResults.value = queryResults
// Initialize selectedMemoryId if not set or update if selection changed
if (!selectedMemoryId.value && processedResults.value.length) {
selectedMemoryId.value = processedResults.value[0].id
}
generateChartData()
}
// Generate chart data
function generateChartData() {
// Add calculated properties to the memory items for easier access
processedResults.value = processedResults.value.map((item) => {
const ageInDays = Math.round(item.age_in_seconds / (24 * 60 * 60))
return {
...item,
age_in_days: ageInDays,
joyComponent: item.joy_score * joyBoostFactor.value,
aversionComponent: item.aversion_score * aversionSpikeFactor.value,
effective_score: item.decayed_score,
}
})
}
// Handle simulated retrieval with emotional response
async function handleRetrieval(memoryId, { joyModifier = 0, aversionModifier = 0 } = {}) {
await simulateEmotionalRetrieval(db.value, memoryId, new Date(Date.now() + simulatedTimeOffset.value * 1000), {
joyModifier,
aversionModifier,
})
await runDecayQuery()
}
// New function to process random retrievals
async function processRandomRetrievals() {
// Only run if database is initialized
if (!db.value || !processedResults.value?.length)
return
// For each memory, check if it should be randomly retrieved based on randomRecallProbability
for (const memory of processedResults.value) {
// Apply the same random check that's in the SQL
if (Math.random() < randomRecallProbability.value) {
// Determine if this should be a joy or aversion-based recall
// Use the existing emotional components to guide this
let joyModifier = 0
let aversionModifier = 0
if (memory.joy_score > memory.aversion_score && memory.joy_score > 0.3) {
joyModifier = 0.05
}
else if (memory.aversion_score > 0.3) {
aversionModifier = 0.05
}
// Call the regular retrieval function
await simulateEmotionalRetrieval(
db.value,
memory.id,
new Date(Date.now() + simulatedTimeOffset.value * 1000),
{ joyModifier, aversionModifier },
)
}
}
// Update the UI
await runDecayQuery()
}
// Handle memory projection reset
async function resetProjection() {
await loadEmotionalSampleData(db.value) // true flag to reset data
simulatedTimeOffset.value = 0
await runDecayQuery()
}
// Handle time jump
async function handleTimeJump({ amount, unit }) {
let secondsToAdd = 0
switch (unit) {
case 'hour':
secondsToAdd = 60 * 60
break
case 'day':
secondsToAdd = 24 * 60 * 60
break
case 'week':
secondsToAdd = 7 * 24 * 60 * 60
break
case 'month':
secondsToAdd = 30 * 24 * 60 * 60
break
case 'year':
secondsToAdd = 365 * 24 * 60 * 60
break
}
simulatedTimeOffset.value += amount * secondsToAdd
await runDecayQuery()
}
// Time acceleration handling
function baseTickTime() {
if (!isTimeAccelerated.value)
return
const now = Date.now()
const elapsedMs = now - lastTickTime.value
simulatedTimeOffset.value += (elapsedMs / 1000) * timeMultiplier.value
lastTickTime.value = now
runDecayQuery()
// Process random retrievals based on elapsed time
// We'll check for random retrievals every few seconds of simulated time
if (Math.random() < (elapsedMs / 5000) * timeMultiplier.value) {
processRandomRetrievals()
}
}
const debouncedTickTime = useDebounceFn(() => {
baseTickTime()
requestAnimationFrame(debouncedTickTime)
}, 1000)
// Add a button to manually trigger random retrievals for testing
async function triggerRandomRetrievals() {
await processRandomRetrievals()
}
// Watch time acceleration state
watch(isTimeAccelerated, (newValue) => {
if (newValue) {
lastTickTime.value = Date.now()
debouncedTickTime()
}
})
// Watch time multiplier changes
watch(timeMultiplier, () => {
if (isTimeAccelerated.value) {
lastTickTime.value = Date.now()
}
})
// Lifecycle hooks
onMounted(async () => {
await initialize()
isTimeAccelerated.value = true
})
onUnmounted(() => {
isTimeAccelerated.value = false
db.value?.$client.then(client => client.close())
})
</script>
<template>
<div>
<template v-if="!isMigrated">
<!-- Loading state -->
<div class="py-8 text-center">
<div class="mx-auto h-8 w-8 animate-spin border-4 border-blue-500 border-t-transparent rounded-full" />
<p class="mt-4">
Initializing database and sample data...
</p>
</div>
</template>
<template v-else>
<header class="mb-4">
<h1 class="mb-2 text-2xl font-bold">
Memory Flashback & Retrieval Simulator
</h1>
<div class="flex items-center justify-between">
<p class="max-w-2xl text-neutral-600 dark:text-neutral-300">
Visualize how memories could be retrieved and impacted by emotional state
</p>
</div>
</header>
<!-- Time Controls -->
<div class="my-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
<h2 class="mb-2 flex items-center text-lg font-semibold" gap-4>
<div flex-1>
Time Simulation
</div>
<div class="text-sm font-mono">
{{ currentSimulatedTime.toLocaleString() }}
</div>
<button
:class="{ 'bg-red-100 dark:bg-red-900': isTimeAccelerated, 'bg-green-100 dark:bg-green-900': !isTimeAccelerated }"
class="rounded-lg px-4 py-2 font-medium transition-colors"
@click="isTimeAccelerated = !isTimeAccelerated"
>
<div v-if="isTimeAccelerated" i-solar:pause-bold />
<div v-else i-solar:play-bold />
</button>
<button
class="rounded-lg bg-neutral-200 px-4 py-2 font-medium dark:bg-neutral-700"
@click="resetProjection"
>
<div i-solar:restart-line-duotone />
</button>
</h2>
<!-- Time jump shortcuts -->
<div class="mt-4 flex flex-wrap gap-2">
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="handleTimeJump({ amount: 1, unit: 'day' })"
>
+1 Day
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="handleTimeJump({ amount: 1, unit: 'week' })"
>
+1 Week
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="handleTimeJump({ amount: 1, unit: 'month' })"
>
+1 Month
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="handleTimeJump({ amount: 1, unit: 'year' })"
>
+1 Year
</button>
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-sm dark:bg-blue-900"
@click="handleTimeJump({ amount: 5, unit: 'year' })"
>
+5 Years
</button>
<!-- New button to trigger random retrievals -->
<button
class="rounded-lg bg-purple-100 px-3 py-1 text-sm dark:bg-purple-900"
title="Trigger random memory recalls based on probability settings"
@click="triggerRandomRetrievals"
>
Random Recalls
</button>
</div>
<!-- Speed controls -->
<div class="mt-4">
<h3 class="font-medium">
Speed
</h3>
<div class="grid grid-cols-2 mt-2 gap-2 md:grid-cols-6 sm:grid-cols-3">
<button
v-for="(speed, index) in [
{ label: '1 second/s', value: 1 },
{ label: '1 minute/s', value: 60 },
{ label: '1 hour/s', value: 60 * 60 },
{ label: '1 day/s', value: 60 * 60 * 24 },
{ label: '1 week/s', value: 60 * 60 * 24 * 7 },
{ label: '1 month/s', value: 60 * 60 * 24 * 30 },
]"
:key="index"
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
:class="timeMultiplier === speed.value ? 'bg-blue-200 dark:bg-blue-800' : 'bg-blue-100 dark:bg-blue-900'"
@click="timeMultiplier = speed.value"
>
{{ speed.label }}
</button>
</div>
</div>
</div>
<div>
<MemoryRetrievalHeatmap
:memory-data="processedResults"
:simulated-time-offset="simulatedTimeOffset"
:time-range="heatmapTimeRange"
class="mb-4"
/>
</div>
<div>
<!-- Chart and Detail View -->
<div class="grid grid-cols-1 mb-4 gap-4 lg:grid-cols-3">
<!-- Chart -->
<EmotionalMemoryChart
v-if="selectedMemory"
:memory-data="processedResults"
:selected-memory-id="selectedMemoryId"
:decay-rate="decayRate"
:max-days-to-project="maxDaysToProject"
:long-term-threshold="longTermThreshold"
:muscle-memory-threshold="muscleMemoryThreshold"
:joy-boost-factor="joyBoostFactor"
:joy-decay-steepness="joyDecaySteepness"
:aversion-spike-factor="aversionSpikeFactor"
:aversion-stability="aversionStability"
class="lg:col-span-2"
/>
<!-- Memory Details -->
<EmotionalMemoryDetail
v-if="selectedMemory"
:memory="selectedMemory"
:long-term-threshold="longTermThreshold"
:muscle-memory-threshold="muscleMemoryThreshold"
@retrieve="handleRetrieval"
/>
</div>
<div class="my-4">
<label class="mb-1 block text-sm font-medium">Heatmap Time Range (days)</label>
<div class="flex items-center gap-2">
<Range
v-model="heatmapTimeRange"
:min="7"
:max="120"
:step="1"
class="w-full"
/>
<span class="w-12 text-right font-mono">{{ heatmapTimeRange }}</span>
</div>
</div>
<!-- Time Controls -->
<div class="mb-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800/50">
<EmotionalSettings
v-model:joy-boost-factor="joyBoostFactor"
v-model:joy-decay-steepness="joyDecaySteepness"
v-model:aversion-spike-factor="aversionSpikeFactor"
v-model:aversion-stability="aversionStability"
v-model:random-recall-probability="randomRecallProbability"
v-model:flashback-intensity="flashbackIntensity"
/>
</div>
<!-- Only show table if viewMode is table or combined -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
<thead class="bg-neutral-100 dark:bg-neutral-800">
<tr>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Rank
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
ID
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Base Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Joy Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Aversion Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Retrievals
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Age (days)
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Effective Score
</th>
<th class="px-4 py-3 text-left text-xs text-neutral-500 font-medium tracking-wider uppercase">
Actions
</th>
</tr>
</thead>
<tbody v-if="processedResults.length" class="bg-white divide-y divide-neutral-200 dark:bg-neutral-900 dark:divide-neutral-800">
<tr
v-for="(memory, idx) in processedResults"
:key="memory.id"
class="cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': memory.id === selectedMemoryId }"
@click="selectedMemoryId = memory.id"
>
<td class="whitespace-nowrap px-4 py-2 text-sm font-medium">
{{ idx + 1 }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ memory.id }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ Math.round(memory.score) }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
<span :class="memory.joy_score > 0.5 ? 'text-yellow-600 dark:text-yellow-400' : ''">
{{ Math.round(memory.joy_score * 100) }}%
</span>
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
<span :class="memory.aversion_score > 0.5 ? 'text-red-600 dark:text-red-400' : ''">
{{ Math.round(memory.aversion_score * 100) }}%
</span>
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ memory.retrieval_count }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm">
{{ Math.round(memory.age_in_seconds / (24 * 60 * 60)) }}
</td>
<td class="whitespace-nowrap px-4 py-2 text-sm font-medium">
{{ Math.round(memory.decayed_score) }}
</td>
<td class="whitespace-nowrap px-4 py-2">
<button
class="rounded-lg bg-blue-100 px-3 py-1 text-xs font-medium dark:bg-blue-900"
@click.stop="handleRetrieval(memory.id)"
>
Retrieve
</button>
</td>
</tr>
</tbody>
<tbody v-else class="bg-white dark:bg-neutral-900">
<tr>
<td colspan="9" class="px-4 py-8 text-center text-neutral-500">
No memory data available
</td>
</tr>
</tbody>
</table>
</div>
<!-- SQL Query Preview (Optional for debugging) -->
<div v-if="showAdvancedSettings" class="mt-6 max-w-full rounded-xl bg-neutral-50 dark:bg-neutral-800">
<div class="overflow-x-scroll rounded bg-neutral-800 p-4 text-sm text-neutral-200">
<pre class="whitespace-pre-wrap">
<code>{{ emotionalDecayQuery }}</code>
</pre>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -1,14 +0,0 @@
:root {
--theme-colors-hue: 255;
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
--theme-colors-chroma-200: calc(var(--theme-colors-chroma) * 0.6);
--theme-colors-chroma-300: calc(var(--theme-colors-chroma) * 0.75);
--theme-colors-chroma-400: var(--theme-colors-chroma);
--theme-colors-chroma-600: calc(var(--theme-colors-chroma) * 1.15);
--theme-colors-chroma-700: calc(var(--theme-colors-chroma) * 1.1);
--theme-colors-chroma-800: calc(var(--theme-colors-chroma) * 0.85);
--theme-colors-chroma-900: calc(var(--theme-colors-chroma) * 0.7);
--theme-colors-chroma-950: calc(var(--theme-colors-chroma) * 0.5);
}
@@ -1,31 +0,0 @@
export interface EmotionalMemoryItem {
// Basic memory properties
id: string
score: number
decayed_score: number
updated_at: string
last_retrieved_at: string
retrieval_count: number
// Emotional components
joy_score: number
aversion_score: number
// Timing information
updated_at_str?: string
last_retrieved_str?: string
age_in_seconds: number
time_since_retrieval: number
// Memory status factors
ltm_factor?: number // Long-term memory factor (0-1)
// Optional calculated components for display
age_in_days?: number
effective_score?: number
}
export interface EmotionalRetrievalModifiers {
joyModifier: number
aversionModifier: number
}
@@ -1,13 +0,0 @@
export interface MemoryItem {
id: string
score: number
updated_at: string // ISO timestamp
last_retrieved_at: string // ISO timestamp
retrieval_count: number
updated_at_str: string
last_retrieved_str: string
age_in_seconds: number
time_since_retrieval: number
ltm_factor?: number // Optional since it's only present when longTermMemoryEnabled is true
decayed_score: number
}
@@ -1 +0,0 @@
export { getBundles } from '@proj-airi/duckdb-wasm/bundles/default-browser'
@@ -1 +0,0 @@
export { getBundles } from '@proj-airi/duckdb-wasm/bundles/default-node'
@@ -1 +0,0 @@
export { getImportUrlBundles } from '@proj-airi/duckdb-wasm/bundles/import-url-browser'
@@ -1 +0,0 @@
export { getImportUrlBundles } from '@proj-airi/duckdb-wasm/bundles/import-url-node'
@@ -1,231 +0,0 @@
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
import { DBStorageType } from '@proj-airi/duckdb-wasm'
import { afterAll, beforeAll, describe, expect, it, onTestFinished } from 'vitest'
import { drizzle } from '.'
import { getImportUrlBundles } from './bundles/import-url-browser'
describe('drizzle with duckdb wasm in browser', { timeout: 10000 }, async () => {
beforeAll(async () => {
const opfsRoot = await navigator.storage.getDirectory()
for await (const name of opfsRoot.keys()) {
if (name.startsWith('drizzle_test_')) {
await opfsRoot.removeEntry(name)
}
}
})
afterAll(async () => {
const opfsRoot = await navigator.storage.getDirectory()
for await (const name of opfsRoot.keys()) {
if (name.startsWith('drizzle_test_')) {
await opfsRoot.removeEntry(name)
}
}
})
it('should have navigator.storage.getDirectory', async () => {
const getDirectory = navigator.storage?.getDirectory
expect(typeof getDirectory).toBe('function')
})
it('should connect to an in-memory DuckDB WASM database', async () => {
const db = drizzle({ connection: { bundles: getImportUrlBundles() } })
const res = await db.execute('SELECT count(*)::INTEGER as v FROM generate_series(0, 100) t(v)')
expect(res).toBeDefined()
expect(res).toEqual([{ v: 101 }])
})
// TODO: Enable this test when DuckDB no longer creates files in read-only mode
it.skip('should fail to open a non-existent OPFS database', async () => {
const db = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'drizzle_test_non_existent',
accessMode: DuckDBAccessMode.READ_ONLY,
},
},
})
// No need to close as the DB will fail to open
await expect(db.$client).rejects.toThrow(/file or directory could not be found/)
const opfsRoot = await navigator.storage.getDirectory()
const nonExistentFileHandle = opfsRoot.getFileHandle('drizzle_test_non_existent', { create: false })
await expect(nonExistentFileHandle).rejects.toThrow(/file or directory could not be found/)
})
it('should create and open an OPFS database', async () => {
const path = `drizzle_test_${crypto.randomUUID().replace(/-/g, '')}`
const db = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `${path}`,
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
onTestFinished(async () => (await db.$client).close())
await expect(db.$client).resolves.toBeDefined()
})
it('should not create an OPFS database with an empty path', async () => {
const db = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: '',
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
// No need to close as the DB will fail to open
await expect(db.$client).rejects.toThrow(/Name is not allowed/)
})
it('should not create an OPFS database with an invalid path', async () => {
const path = `//drizzle_test_${crypto.randomUUID().replace(/-/g, '')}`
const db = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `${path}`,
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
// No need to close as the DB will fail to open
await expect(db.$client).rejects.toThrow(/Name is not allowed/)
})
it('should open, update, save, and reload an OPFS database', async () => {
const path = `drizzle_test_${crypto.randomUUID().replace(/-/g, '')}`
const db1 = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `${path}`,
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
onTestFinished(async () => (await db1.$client).close())
await expect(db1.$client).resolves.toBeDefined()
expect(await db1.execute('SHOW TABLES')).toEqual([])
await expect(db1.execute('CREATE TABLE test (v INTEGER)')).resolves.toBeDefined()
await expect(db1.execute('INSERT INTO test VALUES (1), (2), (3)')).resolves.toBeDefined()
expect(await db1.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
await expect(db1.execute('CHECKPOINT')).resolves.toBeDefined()
await expect((await db1.$client).close()).resolves.toBeUndefined()
const db2 = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `${path}`,
accessMode: DuckDBAccessMode.READ_ONLY,
},
},
})
onTestFinished(async () => (await db2.$client).close())
expect(await db2.execute('SHOW TABLES')).toEqual([{ name: 'test' }])
expect(await db2.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
})
it('should create open the same OPFS database with or without a leading slash', async () => {
const path = `drizzle_test_${crypto.randomUUID().replace(/-/g, '')}`
const db1 = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `${path}`,
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
onTestFinished(async () => (await db1.$client).close())
await expect(db1.$client).resolves.toBeDefined()
expect(await db1.execute('SHOW TABLES')).toEqual([])
await expect(db1.execute('CREATE TABLE test (v INTEGER)')).resolves.toBeDefined()
await expect(db1.execute('INSERT INTO test VALUES (1), (2), (3)')).resolves.toBeDefined()
expect(await db1.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
await expect(db1.execute('CHECKPOINT')).resolves.toBeDefined()
await expect((await db1.$client).close()).resolves.toBeUndefined()
const db2 = drizzle({
connection: {
bundles: getImportUrlBundles(),
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: `/${path}`,
accessMode: DuckDBAccessMode.READ_ONLY,
},
},
})
onTestFinished(async () => (await db2.$client).close())
expect(await db2.execute('SHOW TABLES')).toEqual([{ name: 'test' }])
expect(await db2.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
})
it('should create, open, update, save, and reload an OPFS database with DSN', async () => {
const path = `drizzle_test_${crypto.randomUUID().replace(/-/g, '')}`
const dsn = `duckdb-wasm:///${path}?bundles=import-url&storage=origin-private-fs&write=true`
const db1 = drizzle(dsn)
onTestFinished(async () => (await db1.$client).close())
await expect(db1.$client).resolves.toBeDefined()
expect(await db1.execute('SHOW TABLES')).toEqual([])
await expect(db1.execute('CREATE TABLE test (v INTEGER)')).resolves.toBeDefined()
await expect(db1.execute('INSERT INTO test VALUES (1), (2), (3)')).resolves.toBeDefined()
expect(await db1.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
await expect(db1.execute('CHECKPOINT')).resolves.toBeDefined()
await expect((await db1.$client).close()).resolves.toBeUndefined()
const db2 = drizzle(dsn)
onTestFinished(async () => (await db2.$client).close())
expect(await db2.execute('SHOW TABLES')).toEqual([{ name: 'test' }])
expect(await db2.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
})
it('should create a table with a float array column', async () => {
const db = drizzle({ connection: { bundles: getImportUrlBundles() } })
await db.execute('CREATE TABLE test (v FLOAT[26880], v2 text)')
await db.execute('INSERT INTO test VALUES (1, 2, 3, 4, "test")')
const res = await db.execute('SELECT * FROM test')
expect(res).toEqual([{ v: [1, 2, 3, 4], v2: 'test' }])
})
})
@@ -1,115 +0,0 @@
import type { DBNodeFS } from '@proj-airi/duckdb-wasm'
import { randomUUID } from 'node:crypto'
import { readdir, unlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
import { DBStorageType } from '@proj-airi/duckdb-wasm'
import { getBundles } from '@proj-airi/duckdb-wasm/bundles/default-node'
import { afterAll, beforeAll, describe, expect, it, onTestFinished } from 'vitest'
import { drizzle } from '.'
describe('drizzle with duckdb wasm in node', { timeout: 10000 }, async () => {
beforeAll(async () => {
const tmp = tmpdir()
await Promise.all(
(await readdir(tmp)).reduce<Promise<void>[]>((tasks, filename) => {
if (filename.startsWith('drizzle_test_')) {
tasks.push(unlink(path.join(tmp, filename)))
}
return tasks
}, []),
)
})
afterAll(async () => {
const tmp = tmpdir()
await Promise.all(
(await readdir(tmp)).reduce<Promise<void>[]>((tasks, filename) => {
if (filename.startsWith('drizzle_test_')) {
tasks.push(unlink(path.join(tmp, filename)))
}
return tasks
}, []),
)
})
it('should connect to an in-memory DuckDB WASM database', async () => {
const db = drizzle({ connection: { bundles: getBundles() } })
const res = await db.execute('SELECT count(*)::INTEGER as v FROM generate_series(0, 100) t(v)')
expect(res).toBeDefined()
expect(res).toEqual([{ v: 101 }])
})
it('should open a DuckDB WASM database in Node FS', async () => {
const tmp = tmpdir()
const filename = `drizzle_test_${randomUUID().replace(/-/g, '')}`
const db = drizzle({
connection: {
bundles: getBundles(),
storage: {
type: DBStorageType.NODE_FS,
path: path.resolve(tmp, filename),
accessMode: DuckDBAccessMode.READ_WRITE,
} as DBNodeFS,
},
})
await expect(db.$client).resolves.toBeDefined()
})
it('should open, update, save, and reload an OPFS database', async () => {
const tmp = tmpdir()
const filename = `drizzle_test_${randomUUID().replace(/-/g, '')}`
const db1 = drizzle({
connection: {
bundles: getBundles(),
storage: {
type: DBStorageType.NODE_FS,
path: path.resolve(tmp, filename),
accessMode: DuckDBAccessMode.READ_WRITE,
},
},
})
onTestFinished(async () => (await db1.$client).close())
await expect(db1.$client).resolves.toBeDefined()
expect(await db1.execute('SHOW TABLES')).toEqual([])
await expect(db1.execute('CREATE TABLE test (v INTEGER)')).resolves.toBeDefined()
await expect(db1.execute('INSERT INTO test VALUES (1), (2), (3)')).resolves.toBeDefined()
expect(await db1.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
await expect(db1.execute('CHECKPOINT')).resolves.toBeDefined()
await expect((await db1.$client).close()).resolves.toBeUndefined()
const db2 = drizzle({
connection: {
bundles: getBundles(),
storage: {
type: DBStorageType.NODE_FS,
path: path.resolve(tmp, filename),
accessMode: DuckDBAccessMode.READ_ONLY,
},
},
})
onTestFinished(async () => (await db2.$client).close())
expect(await db2.execute('SHOW TABLES')).toEqual([{ name: 'test' }])
expect(await db2.execute('SELECT * FROM test')).toEqual([{ v: 1 }, { v: 2 }, { v: 3 }])
})
it('should create a table with a float array column', async () => {
const db = drizzle({ connection: { bundles: getBundles() } })
await db.execute('CREATE TABLE vector_test_table (v FLOAT[26880], v2 text)')
await db.execute(`INSERT INTO vector_test_table VALUES (${JSON.stringify(Array.from({ length: 26880 }).fill(1))}, 'text')`)
const res = await db.execute('SELECT * FROM vector_test_table')
expect(res).toEqual([{ v: Array.from({ length: 26880 }).fill(1), v2: 'text' }])
})
})
-153
View File
@@ -1,153 +0,0 @@
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
import type { ConnectOptions, DuckDBWasmClient } from '@proj-airi/duckdb-wasm'
import type { DrizzleConfig, RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm'
import type { DuckDBWasmQueryResultHKT } from './session'
import { ConsoleLogger } from '@duckdb/duckdb-wasm'
import { connect, getEnvironment } from '@proj-airi/duckdb-wasm'
import { createTableRelationsHelpers, DefaultLogger, entityKind, extractTablesRelationalConfig, isConfig } from 'drizzle-orm'
import { PgDatabase, PgDialect } from 'drizzle-orm/pg-core'
import { parseDSN } from './dsn'
import { DuckDBWasmSession } from './session'
export class DuckDBWasmDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> extends PgDatabase<DuckDBWasmQueryResultHKT, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmDatabase'
}
function construct<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
>(
client: Promise<DuckDBWasmClient>,
config: DrizzleConfig<TSchema> = {},
): DuckDBWasmDrizzleDatabase<TSchema, TClient> {
const dialect = new PgDialect({ casing: config.casing })
let logger
if (config.logger === true) {
logger = new DefaultLogger()
}
else if (config.logger !== false) {
logger = config.logger
}
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
)
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
}
}
const session = new DuckDBWasmSession(client, dialect, schema, { logger })
const db = new DuckDBWasmDatabase(dialect, session, schema as any) as DuckDBWasmDatabase<TSchema>;
(<any>db).$client = client
return db as any
}
export interface DuckDBWasmDrizzleDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
> extends DuckDBWasmDatabase<TSchema> {
$client: TClient
}
async function getBundles(importUrl = false): Promise<DuckDBBundles> {
const env = await getEnvironment()
switch (env) {
case 'browser':
return importUrl
? (await import('@proj-airi/duckdb-wasm/bundles/import-url-browser')).getImportUrlBundles()
: (await import('@proj-airi/duckdb-wasm/bundles/default-browser')).getBundles()
case 'node':
return importUrl
? await (await import('@proj-airi/duckdb-wasm/bundles/import-url-node')).getImportUrlBundles()
: await (await import('@proj-airi/duckdb-wasm/bundles/default-node')).getBundles()
default:
throw new Error(`Unsupported environment: "${env}"`)
}
}
function constructByDSN<
TSchema extends Record<string, unknown> = Record<string, never>,
>(
dsn: string,
drizzleConfig?: DrizzleConfig<TSchema>,
): DuckDBWasmDrizzleDatabase<TSchema, Promise<DuckDBWasmClient>> {
const structured = parseDSN(dsn)
return construct(connect({
bundles: getBundles(structured.bundles === 'import-url'),
logger: structured.logger ? new ConsoleLogger() : undefined,
storage: structured.storage,
}), drizzleConfig) as any
}
export function drizzle<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
>(
...params:
| [{ connection: string | ConnectOptions }]
| [{ connection: string | ConnectOptions }, DrizzleConfig<TSchema>]
| [{ client: TClient }]
| [{ client: TClient }, DrizzleConfig<TSchema>]
| [ TClient | string ]
| [ TClient | string, DrizzleConfig<TSchema> ]
): DuckDBWasmDrizzleDatabase<TSchema, TClient> {
if (typeof params[0] === 'string') {
return constructByDSN(params[0] as string, params[1]) as any
}
if (isConfig(params[0])) {
const {
connection,
client,
...drizzleConfig
} = params[0] as {
connection?: string | ConnectOptions // a DSN or a ConnectOptions object
client?: TClient
} & DrizzleConfig<TSchema>
if (client)
return construct(client, drizzleConfig) as any
if (typeof connection === 'string')
return constructByDSN(connection, drizzleConfig) as any
if (typeof connection === 'undefined')
throw new Error('connection option is required')
return construct(connect({
bundles: connection.bundles,
logger: connection.logger,
storage: connection.storage,
}), drizzleConfig) as any
}
return construct(params[0] as TClient, params[1] as DrizzleConfig<TSchema> | undefined) as any
}
// eslint-disable-next-line ts/no-namespace
export namespace drizzle {
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
config?: DrizzleConfig<TSchema>,
): DuckDBWasmDatabase<TSchema> & {
$client: '$client is not available on drizzle.mock()'
} {
return construct({
options: {
parsers: {},
serializers: {},
},
} as any, config) as any
}
}
@@ -1,142 +0,0 @@
import type { DBOriginPrivateFS } from '@proj-airi/duckdb-wasm'
import type { StructuredDSN } from './dsn'
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
import { DBStorageType } from '@proj-airi/duckdb-wasm'
import { describe, expect, it } from 'vitest'
import { buildDSN, parseDSN } from './dsn'
import { spyConsoleWarn } from './test-utils'
describe('parseDSN', { timeout: 10000 }, async () => {
it('should fail with non-duckdb-wasm protocol', async () => {
const dsn = 'random-db:///database.db'
expect(() => parseDSN(dsn)).toThrow('Expected scheme to be "duckdb-wasm:" but got "random-db:"')
})
it('should parse OPFS with path', async () => {
const dsn = 'duckdb-wasm:///path/to/database.db?storage=origin-private-fs'
let structured: StructuredDSN
expect(() => structured = parseDSN(dsn)).not.toThrow()
expect(structured).toEqual({
scheme: 'duckdb-wasm:',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'path/to/database.db',
} as DBOriginPrivateFS,
})
})
it('should parse OPFS with missing leading slash', async () => {
const consoleWarnMock = spyConsoleWarn()
const dsn = 'duckdb-wasm://path/to/database.db?storage=origin-private-fs'
// ^~~~~ missing leading slash: this will be parsed as host/hostname
let structured: StructuredDSN
expect(() => structured = parseDSN(dsn)).not.toThrow()
expect(structured).toEqual({
scheme: 'duckdb-wasm:',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'to/database.db',
} as DBOriginPrivateFS,
})
expect(consoleWarnMock).toHaveBeenCalledTimes(1)
expect(consoleWarnMock).toHaveBeenCalledWith('Host "path" will be ignored while using Origin Private FS')
})
it('should parse OPFS with path and (write = true)', async () => {
const dsn = 'duckdb-wasm:///path/to/database.db?storage=origin-private-fs&write=true'
const structured = parseDSN(dsn)
expect(structured).toEqual({
scheme: 'duckdb-wasm:',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'path/to/database.db',
accessMode: DuckDBAccessMode.READ_WRITE,
} as DBOriginPrivateFS,
})
})
it('should parse OPFS with path and (write = false)', async () => {
const dsn = 'duckdb-wasm:///path/to/database.db?storage=origin-private-fs&write=false'
const structured = parseDSN(dsn)
expect(structured).toEqual({
scheme: 'duckdb-wasm:',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'path/to/database.db',
} as DBOriginPrivateFS,
})
})
})
describe('buildDSN', { timeout: 10000 }, async () => {
it('should build DSN with OPFS (ro)', async () => {
const structured: StructuredDSN = {
scheme: 'duckdb-wasm:',
bundles: 'import-url',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'path/to/database.db',
} as DBOriginPrivateFS,
}
let url: URL
expect(() => url = new URL(buildDSN(structured))).not.toThrow()
expect(url.protocol).toBe('duckdb-wasm:')
expect(url.host).toBe('')
expect(url.pathname).toBe('/path/to/database.db')
expect(url.searchParams.get('storage')).toBe(DBStorageType.ORIGIN_PRIVATE_FS)
expect(url.searchParams.get('write')).toBeNull()
expect(url.searchParams.get('bundles')).toBe('import-url')
})
it('should build DSN with OPFS (rw)', async () => {
const structured: StructuredDSN = {
scheme: 'duckdb-wasm:',
bundles: 'import-url',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: 'path/to/database.db',
accessMode: DuckDBAccessMode.READ_WRITE,
} as DBOriginPrivateFS,
}
let url: URL
expect(() => url = new URL(buildDSN(structured))).not.toThrow()
expect(url.protocol).toBe('duckdb-wasm:')
expect(url.host).toBe('')
expect(url.pathname).toBe('/path/to/database.db')
expect(url.searchParams.get('storage')).toBe(DBStorageType.ORIGIN_PRIVATE_FS)
expect(url.searchParams.get('write')).toBe('true')
expect(url.searchParams.get('bundles')).toBe('import-url')
})
it('should build the same DSN with OPFS but with a leading slash in the path', async () => {
const structured: StructuredDSN = {
scheme: 'duckdb-wasm:',
bundles: 'import-url',
storage: {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: '/path/to/database.db',
accessMode: DuckDBAccessMode.READ_WRITE,
} as DBOriginPrivateFS,
}
let url: URL
expect(() => url = new URL(buildDSN(structured))).not.toThrow()
expect(url.protocol).toBe('duckdb-wasm:')
expect(url.host).toBe('')
expect(url.pathname).toBe('/path/to/database.db')
expect(url.searchParams.get('storage')).toBe(DBStorageType.ORIGIN_PRIVATE_FS)
expect(url.searchParams.get('write')).toBe('true')
expect(url.searchParams.get('bundles')).toBe('import-url')
})
})
-118
View File
@@ -1,118 +0,0 @@
import type { DBStorage } from '@proj-airi/duckdb-wasm'
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
import { DBStorageType } from '@proj-airi/duckdb-wasm'
export interface StructuredDSN {
scheme: 'duckdb-wasm:'
bundles?: 'import-url'
logger?: boolean
storage?: DBStorage
}
export function isLiterallyTrue(value?: string): boolean {
return typeof value === 'string' && /^true$/i.test(value)
}
/**
* Parse a DuckDB WASM DSN string into a structured object
*
* Examples:
* - `duckdb-wasm:///` -> In-memory
* - `duckdb-wasm:///?bundles=import-url` -> In-memory, use import-URL bundles
* - `duckdb-wasm:///?logger=true` -> In-memory, enable logger
* - `duckdb-wasm://database.db?storage=origin-private-fs&write=true` -> Origin Private FS, RW, database.db
* - `duckdb-wasm:///database.db?storage=origin-private-fs&write=true` -> Origin Private FS, RW, database.db (leading slash is optional)
*
* @param dsn The DSN string to parse
*/
export function parseDSN(dsn: string): StructuredDSN {
const structured: StructuredDSN = {
scheme: 'duckdb-wasm:',
}
const parsed = new URL(dsn)
// The protocol in the URL maps to the scheme in the DSN (URI)
// See: https://developer.mozilla.org/en-US/docs/Web/API/URL/protocol
if (!parsed.protocol.startsWith('duckdb-wasm:')) {
throw new Error(`Expected scheme to be "duckdb-wasm:" but got "${parsed.protocol}"`)
}
if (parsed.searchParams.get('bundles') === 'import-url') {
structured.bundles = 'import-url'
}
const paramLogger = parsed.searchParams.get('logger')
if (paramLogger && isLiterallyTrue(paramLogger)) {
structured.logger = true
}
const paramStorage = parsed.searchParams.get('storage')
switch (paramStorage) {
case DBStorageType.ORIGIN_PRIVATE_FS: {
if (parsed.host.length > 0) {
console.warn(`Host "${parsed.host}" will be ignored while using Origin Private FS`)
}
const paramWrite = parsed.searchParams.get('write')
structured.storage = {
type: DBStorageType.ORIGIN_PRIVATE_FS,
path: parsed.pathname.startsWith('/') ? parsed.pathname.slice(1) : parsed.pathname,
...(paramWrite && isLiterallyTrue(paramWrite) && {
accessMode: DuckDBAccessMode.READ_WRITE,
}),
}
break
}
case null:
break
default:
console.warn(`Unknown storage type "${paramStorage}"`)
break
}
return structured
}
/**
* Build a DuckDB WASM DSN string from a structured DSN object
*
* @param structured The structured DSN object
* @returns The DSN string
*/
export function buildDSN(structured: StructuredDSN): string {
const parsed = new URL('duckdb-wasm:///')
if (structured.bundles === 'import-url') {
parsed.searchParams.set('bundles', 'import-url')
}
if (structured.logger) {
parsed.searchParams.set('logger', 'true')
}
if (structured.storage) {
parsed.searchParams.set('storage', structured.storage.type)
switch (structured.storage.type) {
case DBStorageType.ORIGIN_PRIVATE_FS:
parsed.pathname = structured.storage.path
if (!parsed.pathname.startsWith('/')) {
// To make the pathname pathname in the URL
parsed.pathname = `/${parsed.pathname}`
}
if (structured.storage.accessMode === DuckDBAccessMode.READ_WRITE) {
parsed.searchParams.set('write', 'true')
}
break
case DBStorageType.NODE_FS:
parsed.pathname = structured.storage.path
if (structured.storage.accessMode === DuckDBAccessMode.READ_WRITE) {
parsed.searchParams.set('write', 'true')
}
break
}
}
return parsed.toString()
}
@@ -1,8 +0,0 @@
export * from './driver'
export * from './dsn'
export * from './migrator'
export * from './session'
export type { AsyncDuckDBConnection, DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
export { AsyncDuckDB, ConsoleLogger, getJsDelivrBundles, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
export type { ConnectOptionalOptions, ConnectOptions, ConnectRequiredOptions, DuckDBWasmClient } from '@proj-airi/duckdb-wasm'
export { connect, mapColumnData } from '@proj-airi/duckdb-wasm'
@@ -1,13 +0,0 @@
import type { MigrationConfig } from 'drizzle-orm/migrator'
import type { PgSession } from 'drizzle-orm/pg-core'
import type { DuckDBWasmDatabase } from './driver'
import { readMigrationFiles } from 'drizzle-orm/migrator'
export async function migrate<TSchema extends Record<string, unknown>>(
db: DuckDBWasmDatabase<TSchema>,
config: MigrationConfig,
) {
const migrations = readMigrationFiles(config)
await (db as any).dialect.migrate(migrations, (db as any).session as unknown as PgSession, config)
}
-164
View File
@@ -1,164 +0,0 @@
import type { DuckDBWasmClient } from '@proj-airi/duckdb-wasm'
import type { Assume, Logger, Query, RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm'
import type { PgDialect, PgQueryResultHKT, PgTransactionConfig, PreparedQueryConfig, SelectedFieldsOrdered } from 'drizzle-orm/pg-core'
import { beginTransaction, withSavepoint } from '@proj-airi/duckdb-wasm'
import { entityKind, fillPlaceholders, NoopLogger } from 'drizzle-orm'
import { PgPreparedQuery, PgSession, PgTransaction } from 'drizzle-orm/pg-core'
export type Row = Record<string, any>
export type RowList<T extends Row[]> = T
export class DuckDBWASMPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
static override readonly [entityKind]: string = 'DuckDBWasmPreparedQuery'
constructor(
private client: Promise<DuckDBWasmClient>,
private queryString: string,
private params: unknown[],
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
private customResultMapper?: (rows: unknown[][]) => T['execute'],
) {
super({ sql: queryString, params })
}
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
const params = fillPlaceholders(this.params, placeholderValues)
this.logger.logQuery(this.queryString, params)
const { fields, queryString: query, client, customResultMapper } = this
const c = await client
if (!fields && !customResultMapper) {
return c.query(query, params)
}
return c.query(query, params)
}
async all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
const params = fillPlaceholders(this.params, placeholderValues)
this.logger.logQuery(this.queryString, params)
const c = await this.client
return c.query(this.queryString, params)
}
}
export interface DuckDBWASMSessionOptions {
logger?: Logger
}
export class DuckDBWasmSession<
TSQL extends Promise<DuckDBWasmClient>,
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgSession<DuckDBWasmQueryResultHKT, TFullSchema, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmSession'
logger: Logger
constructor(
public client: TSQL,
dialect: PgDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
readonly options: DuckDBWASMSessionOptions = {},
) {
super(dialect)
this.logger = options.logger ?? new NoopLogger()
}
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
query: Query,
fields: SelectedFieldsOrdered | undefined,
_name: string | undefined,
_isResponseInArrayMode: boolean,
customResultMapper?: (rows: unknown[][]) => T['execute'],
): PgPreparedQuery<T> {
return new DuckDBWASMPreparedQuery(
this.client,
query.sql,
query.params,
this.logger,
fields,
customResultMapper,
)
}
async query(query: string, params: unknown[]): Promise<RowList<Row[]>> {
this.logger.logQuery(query, params)
const c = await this.client
return c.query(query, params)
}
async queryObjects<T extends Row>(
query: string,
params: unknown[],
): Promise<RowList<T[]>> {
this.logger.logQuery(query, params)
const c = await this.client
return c.query(query, params) as Promise<RowList<T[]>>
}
override transaction<T>(
transaction: (tx: DuckDBWasmTransaction<TFullSchema, TSchema>) => Promise<T>,
config?: PgTransactionConfig,
): Promise<T> {
return beginTransaction(this.client, async (client) => {
const session = new DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>(
client,
this.dialect,
this.schema,
this.options,
)
const tx = new DuckDBWasmTransaction(this.dialect, session, this.schema)
if (config) {
await tx.setTransaction(config)
}
return transaction(tx)
}) as Promise<T>
}
}
export class DuckDBWasmTransaction<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgTransaction<DuckDBWasmQueryResultHKT, TFullSchema, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmTransaction'
dialect: PgDialect
session: DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>
constructor(
dialect: PgDialect,
session: DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>,
schema: RelationalSchemaConfig<TSchema> | undefined,
nestedIndex = 0,
) {
super(dialect, session, schema, nestedIndex)
this.dialect = dialect
this.session = session
}
override async transaction<T>(
transaction: (tx: DuckDBWasmTransaction<TFullSchema, TSchema>) => Promise<T>,
): Promise<T> {
return withSavepoint(this.session.client, '', async (client) => {
const session = new DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>(
client,
this.dialect,
this.schema,
this.session.options,
)
const tx = new DuckDBWasmTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema)
return transaction(tx)
}) as Promise<T>
}
}
export interface DuckDBWasmQueryResultHKT extends PgQueryResultHKT {
type: RowList<Assume<this['row'], Row>[]>
}
@@ -1,12 +0,0 @@
import { afterAll, vi } from 'vitest'
export function spyConsoleWarn() {
const hookedConsoleWarn = console.warn
const consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation(hookedConsoleWarn)
afterAll(() => {
consoleWarnMock.mockReset()
})
return consoleWarnMock
}
@@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable",
"DOM.AsyncIterable",
"WebWorker"
],
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@proj-airi/duckdb-wasm/*": [
"../duckdb-wasm/src/*"
],
"@proj-airi/duckdb-wasm": [
"../duckdb-wasm/src/index.ts"
]
},
"types": [
"vite/client",
"@vitest/browser/providers/playwright",
"unplugin-vue-router/client"
],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.mts",
"playground/**/*.ts",
"playground/**/*.d.ts",
"playground/**/*.mts",
"playground/**/*.vue"
]
}
@@ -1,33 +0,0 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetWebFonts,
presetWind3,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
presetTypography(),
presetWebFonts({
fonts: {
sans: 'DM Sans',
serif: 'DM Serif Display',
mono: 'DM Mono',
},
}),
presetIcons({
scale: 1.2,
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
safelist: 'prose prose-sm m-auto text-left'.split(' '),
})
@@ -1,23 +0,0 @@
import { resolve } from 'node:path'
import Vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: resolve(import.meta.dirname, 'playground', 'dist'),
},
plugins: [
// https://github.com/posva/unplugin-vue-router
VueRouter({
root: 'playground',
extensions: ['.vue', '.md'],
dts: resolve(import.meta.dirname, 'playground', 'src', 'typed-router.d.ts'),
}),
Vue(),
// https://github.com/antfu/unocss
// see uno.config.ts for config
Unocss(),
],
})
@@ -1,32 +0,0 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
workspace: [
{
extends: true,
test: {
name: 'node',
environment: 'node',
include: ['**/*.{spec,test}.ts'],
exclude: ['**/*.browser.{spec,test}.ts', '**/node_modules/**'],
},
},
{
extends: true,
test: {
name: 'browser',
include: ['**/*.browser.{spec,test}.ts'],
exclude: ['**/node_modules/**'],
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'chromium' },
],
},
},
},
],
},
})
+2 -67
View File
@@ -1,68 +1,3 @@
# Easy to use `@duckdb/duckdb-wasm` wrapper for both browser and Node.js environments
# We have moved
> [Playground](https://drizzle-orm-duckdb-wasm.netlify.app/)
## Installation
Pick the package manager of your choice:
```shell
ni @proj-airi/duckdb-wasm -D # from @antfu/ni, can be installed via `npm i -g @antfu/ni`
pnpm i @proj-airi/duckdb-wasm -D
yarn i @proj-airi/duckdb-wasm -D
npm i @proj-airi/duckdb-wasm -D
```
## Usage
### Browser
```html
<script setup lang="ts">
import type { DuckDBWasmClient } from '@proj-airi/duckdb-wasm'
import { connect, getEnvironment } from '@proj-airi/duckdb-wasm'
import { getImportUrlBundles } from '@proj-airi/duckdb-wasm/bundles/import-url-browser'
import { onMounted, onUnmounted, ref } from 'vue'
const db = ref<DuckDBWasmClient>()
onMounted(async () => {
db.value = await connect({ bundles: getImportUrlBundles })
const result = await db.value.conn.query('SELECT 1 + 1 AS res')
console.log(result) // Output: [{ res: 2 }]
})
onUnmounted(() => {
db.value?.close()
})
</script>
```
### Node.js
You will need to install `web-worker` too.
```shell
ni web-worker # from @antfu/ni, can be installed via `npm i -g @antfu/ni`
pnpm i web-worker
yarn i web-worker
npm i web-worker
```
```typescript
import { connect, getEnvironment } from '@proj-airi/duckdb-wasm'
import { getImportUrlBundles } from '@proj-airi/duckdb-wasm/bundles/default-node'
async function main() {
const { conn, close } = await connect({ bundles: getImportUrlBundles })
const result = await conn.query('SELECT 1 + 1 AS res')
console.log(result) // Output: [{ res: 2 }]
await close()
}
```
## Footnotes
Check out the [Drizzle ORM driver](https://github.com/moeru-ai/airi/blob/main/packages/drizzle-duckdb-wasm/README.md) we made for `@duckdb/duckdb-wasm` as welL!
Hello! This package has been moved to [proj-airi/duckdb-wasm](https://github.com/proj-airi/duckdb-wasm). You may keep tracking the updates of this package there.
-76
View File
@@ -1,76 +0,0 @@
{
"name": "@proj-airi/duckdb-wasm",
"type": "module",
"version": "0.4.19",
"description": "Easy to use @duckdb/duckdb-wasm wrapper for both browser and Node.js environments",
"author": {
"name": "Neko Ayaka",
"email": "neko@ayaka.moe",
"url": "https://github.com/nekomeowww"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/duckdb-wasm"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"node": "./dist/index.cjs"
},
"./bundles/default-browser": {
"types": "./dist/bundles/default-browser.d.ts",
"import": "./dist/bundles/default-browser.mjs"
},
"./bundles/default-node": {
"types": "./dist/bundles/default-node.d.ts",
"import": "./dist/bundles/default-node.mjs",
"node": "./dist/bundles/default-node.cjs"
},
"./bundles/import-url-browser": {
"types": "./dist/bundles/import-url-browser.d.ts",
"import": "./dist/bundles/import-url-browser.mjs"
},
"./bundles/import-url-node": {
"types": "./dist/bundles/import-url-node.d.ts",
"import": "./dist/bundles/import-url-node.mjs",
"node": "./dist/bundles/import-url-node.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "pnpm run stub",
"stub": "unbuild",
"build": "unbuild",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"web-worker": "^1.5.0"
},
"peerDependenciesMeta": {
"web-worker": {
"optional": true
}
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@duckdb/duckdb-wasm": "1.29.1-dev68.0",
"apache-arrow": "^19.0.1",
"date-fns": "^4.1.0",
"defu": "^6.1.4",
"drizzle-orm": "^0.41.0",
"es-toolkit": "^1.34.1"
},
"devDependencies": {
"drizzle-kit": "^0.30.6"
}
}
@@ -1,19 +0,0 @@
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
export function getBundles(): DuckDBBundles {
return {
mvp: {
mainModule: './duckdb-mvp.wasm',
mainWorker: './duckdb-browser-mvp.worker.js',
},
eh: {
mainModule: './duckdb-eh.wasm',
mainWorker: './duckdb-browser-eh.worker.js',
},
coi: {
mainModule: './duckdb-coi.wasm',
mainWorker: './duckdb-browser-coi.worker.js',
pthreadWorker: './duckdb-browser-coi.pthread.worker.js',
},
}
}
@@ -1,19 +0,0 @@
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
export async function getBundles(): Promise<DuckDBBundles> {
const { createRequire } = await import('node:module')
const { dirname, resolve } = await import('node:path')
const require = createRequire(import.meta.url)
const DUCKDB_DIST = dirname(require.resolve('@duckdb/duckdb-wasm'))
return {
mvp: {
mainModule: resolve(DUCKDB_DIST, './duckdb-mvp.wasm'),
mainWorker: resolve(DUCKDB_DIST, './duckdb-node-mvp.worker.cjs'),
},
eh: {
mainModule: resolve(DUCKDB_DIST, './duckdb-eh.wasm'),
mainWorker: resolve(DUCKDB_DIST, './duckdb-node-eh.worker.cjs'),
},
}
}
@@ -1,28 +0,0 @@
/* eslint-disable perfectionist/sort-imports */
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
import ehMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url'
import ehMainModule from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url'
import mvpMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url'
import mvpMainModule from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url'
import coiMainModule from '@duckdb/duckdb-wasm/dist/duckdb-coi.wasm?url'
import coiMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-coi.worker.js?url'
import coiPthreadWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-coi.pthread.worker.js?url'
export function getImportUrlBundles(): DuckDBBundles {
return {
mvp: {
mainModule: mvpMainModule,
mainWorker: mvpMainWorker,
},
eh: {
mainModule: ehMainModule,
mainWorker: ehMainWorker,
},
coi: {
mainModule: coiMainModule,
mainWorker: coiMainWorker,
pthreadWorker: coiPthreadWorker,
},
}
}
@@ -1,28 +0,0 @@
/* eslint-disable perfectionist/sort-imports */
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
import ehMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-node-eh.worker.cjs?url'
import ehMainModule from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url'
import mvpMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-node-mvp.worker.cjs?url'
import mvpMainModule from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url'
function transformUrl(url: string) {
if (url.startsWith('/@fs/')) {
return url.replace('/@fs/', 'file://')
}
return url
}
export async function getImportUrlBundles(): Promise<DuckDBBundles> {
return {
mvp: {
mainModule: transformUrl(mvpMainModule),
mainWorker: transformUrl(mvpMainWorker),
},
eh: {
mainModule: transformUrl(ehMainModule),
mainWorker: transformUrl(ehMainWorker),
},
}
}
-39
View File
@@ -1,39 +0,0 @@
/**
* A type predicate that is true if the given value is either undefined
* or null.
*/
export function isNullOrUndefined<T>(
value: T | null | undefined,
): value is null | undefined {
return <T>value === null || <T>value === undefined
}
/**
* A type predicate that is true if the given value is neither undefined
* nor null.
*/
export function notNullOrUndefined<T>(
value: T | null | undefined,
): value is T {
return <T>value !== null && <T>value !== undefined
}
export async function getEnvironment() {
if (typeof window !== 'undefined' && window !== null) {
return 'browser'
}
try {
const process = await import('node:process')
if (typeof process !== 'undefined'
&& process.versions != null
&& process.versions.node != null) {
return 'node'
}
return 'unknown'
}
catch {
return 'unknown'
}
}
-173
View File
@@ -1,173 +0,0 @@
import type { AsyncDuckDBConnection, DuckDBBundle, DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
import type { DBStorage } from './storage'
import { AsyncDuckDB, ConsoleLogger, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
import { defu } from 'defu'
import { getEnvironment } from './common'
import { mapStructRowData } from './format'
import { DBStorageType } from './storage'
export type ConnectOptions = ConnectRequiredOptions & ConnectOptionalOptions
export interface ConnectOptionalOptions {
bundles?: DuckDBBundles | Promise<DuckDBBundles>
logger?: boolean | Logger
storage?: DBStorage
}
export interface ConnectRequiredOptions {
}
export interface DuckDBWasmClient {
worker: Worker
db: AsyncDuckDB
conn: AsyncDuckDBConnection
close: () => Promise<void>
query: (query: string, params?: unknown[]) => Promise<Record<string, unknown>[]>
}
export async function connect(options: ConnectOptions): Promise<DuckDBWasmClient> {
const opts = defu(options, { logger: false })
let worker: Worker
let bundle: DuckDBBundle
const env = await getEnvironment()
if (env === 'browser') {
if (typeof opts.bundles === 'undefined') {
const { getBundles } = await import('./bundles/default-browser')
opts.bundles = await getBundles()
}
bundle = await selectBundle(await opts.bundles)
worker = new Worker(bundle.mainWorker!)
}
else if (env === 'node') {
if (typeof opts.bundles === 'undefined') {
const { getBundles } = await import('./bundles/default-node')
opts.bundles = await getBundles()
}
bundle = await selectBundle(await opts.bundles)
let workerUrl = bundle.mainWorker!
if (workerUrl.startsWith('/@fs/')) {
workerUrl = workerUrl.replace('/@fs/', 'file://')
}
const ww = await import('web-worker')
// eslint-disable-next-line new-cap
worker = new ww.default(workerUrl, { type: 'module' })
}
else {
throw new Error(`Unsupported environment: ${env}`)
}
let logger: Logger
if (opts.logger === true) {
logger = new ConsoleLogger()
}
else if (opts.logger === false) {
logger = new VoidLogger()
}
else {
logger = opts.logger
}
const db = new AsyncDuckDB(logger, worker)
await db.instantiate(bundle.mainModule, bundle.pthreadWorker)
if (opts.storage) {
switch (opts.storage.type) {
case DBStorageType.ORIGIN_PRIVATE_FS: {
try {
let strippedPath = opts.storage.path
if (strippedPath.startsWith('/')) {
// We will strip the only leading slash as it is not needed
strippedPath = strippedPath.slice(1)
}
await db.open({
path: `opfs://${strippedPath}`,
accessMode: opts.storage.accessMode,
// OPFS already uses direct IO
})
}
catch (e) {
await db.terminate()
await worker.terminate()
throw e
}
break
}
case DBStorageType.NODE_FS: {
try {
await db.open({
path: opts.storage.path,
accessMode: opts.storage.accessMode,
useDirectIO: true, // Important! Otherwise the file will be created without DB init
})
}
catch (e) {
await db.terminate()
await worker.terminate()
throw e
}
break
}
}
}
const conn = await db.connect()
return {
worker,
db,
conn,
query: async (query: string, params: unknown[] = []) => {
if (!params || params.length === 0) {
const results = await conn.query(query)
return mapStructRowData(results)
}
const stmt = await conn.prepare(query)
const results = await stmt.query(...params)
const rows = mapStructRowData(results)
stmt.close()
return rows
},
close: async () => {
await conn.close()
await db.terminate()
await worker.terminate()
},
}
}
export async function beginTransaction(client: Promise<DuckDBWasmClient>, txFn: (client: Promise<DuckDBWasmClient>) => Promise<any>): Promise<any> {
await (await client).conn.send('BEGIN TRANSACTION')
try {
const result = await txFn(client)
await (await client).conn.send('COMMIT')
return result
}
catch (err) {
await (await client).conn.send('ROLLBACK')
throw err
}
}
export async function withSavepoint(client: Promise<DuckDBWasmClient>, spName: string, txFn: (client: Promise<DuckDBWasmClient>) => Promise<any>): Promise<any> {
await (await client).conn.send(`SAVEPOINT ${spName}`)
try {
const result = await txFn(client)
await (await client).conn.send(`RELEASE SAVEPOINT ${spName}`)
return result
}
catch (err) {
await (await client).conn.send(`ROLLBACK TO SAVEPOINT ${spName}`)
throw err
}
}
-655
View File
@@ -1,655 +0,0 @@
import type { TZDate } from '@date-fns/tz'
import type { Field, Schema, StructRow } from 'apache-arrow'
import type { DataType } from './types'
import { TZDateMini } from '@date-fns/tz'
import { DataType as ArrowDataType, Struct, TimeUnit, util } from 'apache-arrow'
import {
addDays,
addHours,
addMilliseconds,
addMinutes,
addMonths,
addQuarters,
addSeconds,
addWeeks,
addYears,
format as dateFormat,
formatDuration as dateFormatDuration,
fromUnixTime,
setDay,
transpose,
} from 'date-fns'
import { trimEnd } from 'es-toolkit'
import { isNullOrUndefined, notNullOrUndefined } from './common'
import {
isBooleanType,
isDatetimeType,
isDateType,
isDecimalType,
isDurationType,
isFloatType,
isIntegerType,
isIntervalType,
isListType,
isObjectType,
isPeriodType,
isTimeType,
} from './types'
/**
* The frequency strings defined in pandas.
* See: https://pandas.pydata.org/docs/user_guide/timeseries.html#period-aliases
* Not supported: "N" (nanoseconds), "U" & "us" (microseconds), and "B" (business days).
* Reason is that these types are not supported by moment.js, but also they are not
* very commonly used in practice.
*/
type SupportedPandasOffsetType =
// yearly frequency:
| 'A' // deprecated alias
| 'Y'
// quarterly frequency:
| 'Q'
// monthly frequency:
| 'M'
// weekly frequency:
| 'W'
// calendar day frequency:
| 'D'
// hourly frequency:
| 'H' // deprecated alias
| 'h'
// minutely frequency
| 'T' // deprecated alias
| 'min'
// secondly frequency:
| 'S' // deprecated alias
| 's'
// milliseconds frequency:
| 'L' // deprecated alias
| 'ms'
type PandasPeriodFrequency =
| SupportedPandasOffsetType
| `${SupportedPandasOffsetType}-${string}`
const BASE_DATE = new Date(1970, 0, 1) // 1970-01-01
function formatMs(duration: number): string {
return dateFormat(addMilliseconds(BASE_DATE, duration), 'yyyy-MM-dd HH:mm:ss.SSS')
}
function formatSec(duration: number): string {
return dateFormat(addSeconds(BASE_DATE, duration), 'yyyy-MM-dd HH:mm:ss')
}
function formatMin(duration: number): string {
return dateFormat(addMinutes(BASE_DATE, duration), 'yyyy-MM-dd HH:mm')
}
function formatHours(duration: number): string {
return dateFormat(addHours(BASE_DATE, duration), 'yyyy-MM-dd HH:mm')
}
function formatDay(duration: number): string {
return dateFormat(addDays(BASE_DATE, duration), 'yyyy-MM-dd')
}
function formatMonth(duration: number): string {
return dateFormat(addMonths(BASE_DATE, duration), 'yyyy-MM')
}
function formatYear(duration: number): string {
return dateFormat(addYears(BASE_DATE, duration), 'yyyy')
}
function formatWeeks(duration: number, freqParam?: string): string {
if (!freqParam) {
throw new Error('Frequency "W" requires parameter')
}
const WEEKDAY_SHORT = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
const dayIndex = WEEKDAY_SHORT.indexOf(freqParam)
if (dayIndex < 0) {
throw new Error(
`Invalid value: ${freqParam}. Supported values: ${JSON.stringify(
WEEKDAY_SHORT,
)}`,
)
}
const weekDate = addWeeks(BASE_DATE, duration)
const startDate = dateFormat(setDay(weekDate, dayIndex - 6), 'yyyy-MM-dd')
const endDate = dateFormat(setDay(weekDate, dayIndex), 'yyyy-MM-dd')
return `${startDate}/${endDate}`
}
function formatQuarter(duration: number): string {
const date = addQuarters(BASE_DATE, duration)
const year = dateFormat(date, 'yyyy')
const quarter = Math.floor(date.getMonth() / 3) + 1
return `${year}Q${quarter}`
}
/**
* Formatters for the different pandas period frequencies.
*
* This is a mapping from the frequency strings to the function that formats the period.
*/
const PERIOD_TYPE_FORMATTERS: Record<
SupportedPandasOffsetType,
(duration: number, freqParam?: string) => string
> = {
L: formatMs,
ms: formatMs,
S: formatSec,
s: formatSec,
T: formatMin,
min: formatMin,
H: formatHours,
h: formatHours,
D: formatDay,
M: formatMonth,
W: formatWeeks,
Q: formatQuarter,
Y: formatYear,
A: formatYear,
}
/**
* Adjusts a time value to seconds based on the unit information in the field.
*
* The unit numbers are specified here:
* https://github.com/apache/arrow/blob/3ab246f374c17a216d86edcfff7ff416b3cff803/js/src/enum.ts#L95
*
* @param timestamp The timestamp to convert.
* @param unit The unit of the timestamp. 0 is seconds, 1 is milliseconds, 2 is microseconds, 3 is nanoseconds.
* @returns The timestamp in seconds.
*/
function convertTimestampToSeconds(
timestamp: number | bigint,
unit: TimeUnit,
): number {
let unitAdjustment
if (unit === TimeUnit.MILLISECOND) {
// Milliseconds
unitAdjustment = 1000
}
else if (unit === TimeUnit.MICROSECOND) {
// Microseconds
unitAdjustment = 1000 * 1000
}
else if (unit === TimeUnit.NANOSECOND) {
// Nanoseconds
unitAdjustment = 1000 * 1000 * 1000
}
else {
// Interpret it as seconds as a fallback
return Number(timestamp)
}
// Do the calculation based on bigints, if the value
// is a bigint and not safe for usage as number.
// This might lose some precision since it doesn't keep
// fractional parts.
if (
typeof timestamp === 'bigint'
&& !Number.isSafeInteger(Number(timestamp))
) {
return Number(timestamp / BigInt(unitAdjustment))
}
return Number(timestamp) / unitAdjustment
}
/**
* Converts a UTC time value (timestamp) to a date object.
*
* @param timestamp The timestamp to convert.
* @param field The field containing the unit information.
* @returns The date object in UTC timezone.
*/
export function convertTimeToDate(
timestamp: number | bigint,
field?: Field,
): Date {
// Time values from arrow are not converted to a shared unit and
// just return the raw arrow value. Therefore, we need to adjust
// the value to seconds based on the unit information in the field.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L193C7-L209
const timeInSeconds = convertTimestampToSeconds(
timestamp,
// The default is SECOND because that is the default unit for time values in pandas.
// Though we believe that actually always a unit is populated by arrow.
field?.type?.unit ?? TimeUnit.SECOND,
)
return fromUnixTime(timeInSeconds)
}
/**
* Formats a duration value based on the unit information in the field.
*
* @param duration The duration value to format.
* @param field The field containing the unit information.
* @returns The formatted duration value.
*/
export function formatDuration(duration: number | bigint, field?: Field): string {
// unit: 0 is seconds, 1 is milliseconds, 2 is microseconds, 3 is nanoseconds.
return dateFormatDuration({
seconds: convertTimestampToSeconds(
duration,
// The default is NANOSECOND because that is the default unit for duration in pandas.
// Though we believe that actually always a unit is populated by arrow.
field?.type?.unit ?? TimeUnit.NANOSECOND,
),
})
}
/**
* Formats a time value based on the unit information in the field.
*
* @param timestamp The time value to format.
* @param field The field containing the unit information.
* @returns The formatted time value.
*/
export function formatTime(timestamp: number | bigint, field?: Field): string {
const date = convertTimeToDate(timestamp, field)
return dateFormat(
date,
date.getMilliseconds() === 0 ? 'HH:mm:ss' : 'HH:mm:ss.SSS',
)
}
export function formatDate(date: number | Date): string {
// Date values from arrow are already converted to a date object
// or a timestamp in milliseconds even if the field unit belonging to the
// passed date might have indicated a different unit.
// Thats why we don't need the field information here (aka its not passed to the function)
// and we don't need to apply any unit conversion.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L167-L171
const formatPattern = 'yyyy-MM-dd'
if (
!(
date instanceof Date
|| (typeof date === 'number' && Number.isFinite(date))
)
) {
console.warn(`Unsupported date value: ${date}`)
return String(date)
}
return dateFormat(date, formatPattern)
}
/**
* Format datetime value from Arrow to string.
*/
export function formatDatetime(date: number | Date, field?: Field): Date | null {
// Datetime values from arrow are already converted to a date object
// or a timestamp in milliseconds even if the field unit might indicate a
// different unit.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L174-L190
if (
!(
date instanceof Date
|| (typeof date === 'number' && Number.isFinite(date))
)
) {
console.warn(`Unsupported datetime value: ${date}`)
return null
}
let datetime: TZDate
const timezone = field?.type?.timezone
// Impact by upstream changes, see:
// - How to replace date-fns-tz · Issue #9 - https://github.com/date-fns/tz/issues/9
// - Inconsistencies from date-fns-tz · Issue #6 - https://github.com/date-fns/tz/issues/6
if (typeof date === 'number') {
if (timezone) {
datetime = new TZDateMini(date, timezone)
}
else {
datetime = new TZDateMini(date)
}
}
else {
if (timezone) {
datetime = new TZDateMini(date, timezone)
}
else {
datetime = new TZDateMini(date)
}
}
// Return the timestamp without timezone information
return transpose(datetime, Date)
}
/**
* Formats a decimal value with a given scale to a string.
*
* This code is partly based on: https://github.com/apache/arrow/issues/35745
*
* TODO: This is only a temporary workaround until ArrowJS can format decimals correctly.
* This is tracked here:
* https://github.com/apache/arrow/issues/37920
* https://github.com/apache/arrow/issues/28804
* https://github.com/apache/arrow/issues/35745
*/
export function formatDecimal(value: Uint32Array | Int32Array, field?: Field): string {
const scale = field?.type?.scale || 0
// Format Uint32Array to a numerical string and pad it with zeros
// So that it is exactly the length of the scale.
let numString = util.bigNumToString(new util.BN(value)).padStart(scale, '0')
// ArrowJS 13 correctly adds a minus sign for negative numbers.
// but it doesn't handle th fractional part yet. So we can just return
// the value if scale === 0, but we need to do some additional processing
// for the fractional part if scale > 0.
if (scale === 0) {
return numString
}
let sign = ''
if (numString.startsWith('-')) {
// Check if number is negative, and if so remember the sign and remove it.
// We will add it back later.
sign = '-'
numString = numString.slice(1)
}
// Extract the whole number part. If the number is < 1, it doesn't
// have a whole number part, so we'll use "0" instead.
// E.g for 123450 with scale 3, we'll get "123" as the whole part.
const wholePart = numString.slice(0, -scale) || '0'
// Extract the fractional part and remove trailing zeros.
// E.g. for 123450 with scale 3, we'll get "45" as the fractional part.
const decimalPart = trimEnd(numString.slice(-scale), '0') || ''
// Combine the parts and add the sign.
return `${sign}${wholePart}${decimalPart ? `.${decimalPart}` : ''}`
}
const formatter = new Intl.NumberFormat('en-US', { style: 'decimal', maximumFractionDigits: 4, minimumFractionDigits: 4, useGrouping: true })
/**
* Formats a float value to a string.
*
* @param num The float value to format.
* @returns The formatted float value.
*/
export function formatFloat(num: number): string {
if (!Number.isFinite(num)) {
return String(num)
}
return formatter.format(num)
}
// - INTERVAL '24' MONTHS returns [2, 0] as Int32Array, should resolve to '2 years'
// - INTERVAL '1' YEAR returns [1, 0] as Int32Array, should resolve to '1 year'
// - INTERVAL '13' MONTHS returns [1, 1] as Int32Array, should resolve to '1 year 1 month'
// - INTERVAL '1' MONTH returns [0, 1] as Int32Array, should resolve to '1 month'
// - INTERVAL '30' MONTHS returns [2, 6] as Int32Array, should resolve to '2 years 6 months'
function parseInterval(arr: Int32Array) {
const years = arr[0]
const months = arr[1]
const result: string[] = []
if (years !== 0) {
result.push(`${years} year${years > 1 ? 's' : ''}`)
}
if (months !== 0) {
result.push(`${months} month${months > 1 ? 's' : ''}`)
}
return result.length ? result.join(' ') : '0 months'
}
/**
* Formats an interval value from arrow to string.
*/
export function formatInterval(x: DataType, field?: Field): string {
// TODO: still buggy, the return value of interval related fields are always
// [0, 0] as Int32Array, should follow the issue to resolve this.
if (ArrowDataType.isInterval(field?.type)) {
return parseInterval(x as Int32Array)
// However, this doesn't mean that the acceptable value of parseInterval is correct and
// respect field?.type.unit (IntervalUnit), since now the interval units are all
// IntervalUnit.YEAR_MONTH, but @duckdb/duckdb-wasm returned 2 (IntervalUnit.MONTH_DAY_NANO),
// which is not correct.
//
// Experimented with:
// - https://www.quackdb.com/ : Cannot parse
// - https://sekuel.com/playground/?q=U0VMRUNUIElOVEVSVkFMICcxJyBEQVkgQVMgaXQ7 : Cannot parse
// - https://csvfiddle.io/#JTdCJTIyaXNUYWJsZU1ldGFkYXRhT3BlbiUyMiUzQWZhbHNlJTJDJTIyaXNOZXdUYWJsZUZvcm1PcGVuJTIyJTNBZmFsc2UlMkMlMjJpc0NvbmZpcm1EZWxldGVRdWVyeU9wZW4lMjIlM0FmYWxzZSUyQyUyMmlzQ29uZmlybURyb3BUYWJsZU9wZW4lMjIlM0FmYWxzZSUyQyUyMmlzU2hhcmVEaWFsb2dPcGVuJTIyJTNBZmFsc2UlMkMlMjJkYlJlYWR5JTIyJTNBZmFsc2UlMkMlMjJ0YWJsZXMlMjIlM0ElNUIlNUQlMkMlMjJxdWVyaWVzJTIyJTNBJTdCJTIyMCUyMiUzQSU3QiUyMmlkJTIyJTNBMCUyQyUyMnRpdGxlJTIyJTNBJTIyVW50aXRsZWQlMjBxdWVyeSUyMiUyQyUyMmJvZHklMjIlM0ElMjJTRUxFQ1QlMjBJTlRFUlZBTCUyMCcxJyUyMERBWSUyMEFTJTIwaXQlM0IlMjIlMkMlMjJyZXN1bHQlMjIlM0ElNUIlNUQlMkMlMjJlcnJvciUyMiUzQW51bGwlN0QlN0QlMkMlMjJhY3RpdmVRdWVyeUlkJTIyJTNBMCUyQyUyMmFjdGl2ZVRhYmxlTWV0YWRhdGFDb2x1bW5zJTIyJTNBJTVCJTVEJTJDJTIybG9jYWxUYWJsZXNUb1dhcm4lMjIlM0ElNUIlNUQlMkMlMjJpc1F1ZXJ5SW5Qcm9ncmVzcyUyMiUzQWZhbHNlJTJDJTIyZGlkQWRkTmV3VGFibGVTdWNjZWVkJTIyJTNBbnVsbCUyQyUyMmFkZE5ld1RhYmxlRXJyb3IlMjIlM0FudWxsJTdE : Can parse but not even open source
// - https://sidequery.ai/ : Cannot parse
// - https://codapi.org/duckdb/ : Can parse (because based on Go)
//
// const value = formatDecimal(x as Int32Array, field)
// const unit = field?.type.unit
// switch (unit) {
// case IntervalUnit.MONTH_DAY_NANO:
// // In Python:
// // pa.scalar((1, 15, -30), type=pa.month_day_nano_interval())
// // <pyarrow.MonthDayNanoIntervalScalar: MonthDayNano(months=1, days=15, nanoseconds=-30)>
// //
// // see: pyarrow.month_day_nano_interval — Apache Arrow v19.0.0
// // https://arrow.apache.org/docs/python/generated/pyarrow.month_day_nano_interval.html
// return `${value} months`
// case IntervalUnit.DAY_TIME:
// return `${value} days`
// case IntervalUnit.YEAR_MONTH:
// return `${value} years`
// default:
// return value
// }
}
// Serialization for pandas.Interval is provided by Arrow extensions
// https://github.com/pandas-dev/pandas/blob/235d9009b571c21b353ab215e1e675b1924ae55c/
// pandas/core/arrays/arrow/extension_types.py#L17
const extensionName = field && field.metadata.get('ARROW:extension:name')
if (extensionName && extensionName === 'pandas.interval') {
const extensionMetadata = JSON.parse(
field.metadata.get('ARROW:extension:metadata') as string,
)
const { closed } = extensionMetadata
const interval = (x as StructRow).toJSON() as {
left: number
right: number
}
const leftBracket = closed === 'both' || closed === 'left' ? '[' : '('
const rightBracket = closed === 'both' || closed === 'right' ? ']' : ')'
const leftInterval = mapColumnData(interval.left, (field.type as Struct)?.children?.[0])
const rightInterval = mapColumnData(interval.right, (field.type as Struct)?.children?.[1])
return `${leftBracket + leftInterval}, ${rightInterval + rightBracket}`
}
return String(x)
}
export function formatPeriodFromFreq(
duration: number | bigint,
freq: PandasPeriodFrequency,
): string {
const [freqName, freqParam] = freq.split('-', 2)
const momentConverter
= PERIOD_TYPE_FORMATTERS[freqName as SupportedPandasOffsetType]
if (!momentConverter) {
console.warn(`Unsupported period frequency: ${freq}`)
return String(duration)
}
const durationNumber = Number(duration)
if (!Number.isSafeInteger(durationNumber)) {
console.warn(
`Unsupported value: ${duration}. Supported values: [${Number.MIN_SAFE_INTEGER}-${Number.MAX_SAFE_INTEGER}]`,
)
return String(duration)
}
return momentConverter(durationNumber, freqParam)
}
export function formatPeriod(duration: number | bigint, field?: Field): string {
// Serialization for pandas.Period is provided by Arrow extensions
// https://github.com/pandas-dev/pandas/blob/70bb855cbbc75b52adcb127c84e0a35d2cd796a9/pandas/core/arrays/arrow/extension_types.py#L26
if (isNullOrUndefined(field)) {
console.warn('Field information is missing')
return String(duration)
}
const extensionName = field.metadata.get('ARROW:extension:name')
const extensionMetadata = field.metadata.get('ARROW:extension:metadata')
if (
isNullOrUndefined(extensionName)
|| isNullOrUndefined(extensionMetadata)
) {
console.warn('Arrow extension metadata is missing')
return String(duration)
}
if (extensionName !== 'pandas.period') {
console.warn(`Unsupported extension name for period type: ${extensionName}`)
return String(duration)
}
const parsedExtensionMetadata = JSON.parse(extensionMetadata as string)
const { freq } = parsedExtensionMetadata
return formatPeriodFromFreq(duration, freq)
}
/**
* Formats nested arrays and other objects to a JSON string.
*
* @param object The value to format.
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted JSON string.
*/
export function formatObject(object: any, field?: Field): unknown {
if (field?.type instanceof Struct) {
// This type is used by python dictionary values
return JSON.parse(
JSON.stringify(object, (_key, value) => {
if (!notNullOrUndefined(value)) {
// Workaround: Arrow JS adds all properties from all cells
// as fields. When you convert to string, it will contain lots of fields with
// null values. To mitigate this, we filter out null values.
return undefined
}
if (typeof value === 'bigint') {
// JSON.stringify fails to serialize bigint values, therefore we have to
// handle them manually.
// TODO(lukasmasuch): Would it be better to serialize it to a string to
// not lose precision?
return Number(value)
}
return value
}),
)
}
// TODO(lukasmasuch): Investigate if we can unify this with the logic above.
return JSON.parse(
JSON.stringify(object, (_key, value) =>
typeof value === 'bigint' ? Number(value) : value),
)
}
/**
* Takes the cell data and type metadata from arrow and nicely formats it into a human-readable string.
*
* This is mostly a best-effort logic and should not throw exceptions in case of unknown values
* or other issues. This makes it easier to use this method by consumers (table, dataframe) since
* they would have to somehow deal with the exception on a cell level to not crash the full table or app.
*
* @param x The cell value.
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted cell value.
*/
export function mapColumnData<T = unknown>(x: DataType, field?: Field): T {
if (isNullOrUndefined(x)) {
return null as null as unknown as T
}
const isDate = x instanceof Date || Number.isFinite(x)
if (isDate && isDateType(field)) {
return formatDate(x as Date | number) as string as T
}
if (typeof x === 'bigint' && isTimeType(field)) {
return formatTime(Number(x), field) as string as T
}
if (isDate && isDatetimeType(field)) {
return formatDatetime(x as Date | number, field) as Date as T
}
if (isPeriodType(field)) {
// Not supported yet by Postgres and DuckDB
throw new Error('Period type is not supported yet')
}
if (isIntervalType(field)) {
return formatInterval(x, field) as string as T
}
if (isDurationType(field)) {
// Not supported yet by Postgres and DuckDB
throw new Error('Duration type is not supported yet')
}
if (isDecimalType(field)) {
// 'numeric' in Postgres also applies here
return formatDecimal(x as Uint32Array, field) as string as T
}
if (isFloatType(field) && Number.isFinite(x)) {
return x as number as T
}
if (isIntegerType(field)) {
// If int64 or uint64, it should be bigint already,
// if int32 or uint32, it should be number already,
// therefore we can just return the value as is.
return x as number as T
}
if (isObjectType(field) || isListType(field)) {
return formatObject(x, field) as T
}
if (isBooleanType(field)) {
return Boolean(x) as boolean as T
}
return String(x) as T
}
export function mapStructRowData<T extends { toArray: () => StructRow[], schema: Schema }>(results: T) {
const rows = (results.toArray() as StructRow[] || []).map(item => item.toJSON()) || []
const jsRepresentedRows = rows.map((row) => {
results.schema.fields.forEach((field) => {
return row[field.name] = mapColumnData(row[field.name], field)
})
return row
})
return jsRepresentedRows
}
-5
View File
@@ -1,5 +0,0 @@
export * from './common'
export * from './duckdb'
export * from './format'
export * from './storage'
export * from './types'
-37
View File
@@ -1,37 +0,0 @@
import type { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
export enum DBStorageType {
ORIGIN_PRIVATE_FS = 'origin-private-fs',
NODE_FS = 'node-fs',
}
interface PathBasedFS {
path: string
/**
* DuckDB will use {@link DuckDBAccessMode.READ_ONLY} if omitted.
*/
accessMode?: DuckDBAccessMode
}
/**
* Origin Private FS storage.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system
*/
export interface DBOriginPrivateFS extends PathBasedFS {
type: DBStorageType.ORIGIN_PRIVATE_FS
/**
* Path of the file in the Origin Private FS.
*
* Leading slash is not necessary and will be omitted if provided.
*/
path: PathBasedFS['path']
}
export interface DBNodeFS extends PathBasedFS {
type: DBStorageType.NODE_FS
}
export type DBStorage = DBOriginPrivateFS | DBNodeFS
-249
View File
@@ -1,249 +0,0 @@
import type { Dictionary, Field, Struct, StructRow, Vector } from 'apache-arrow'
import { DataType as ArrowDataType } from 'apache-arrow'
import { isNullOrUndefined } from './common'
/** Data types used by ArrowJS. */
export type DataType =
| null
| boolean
| number
| string
| Date // datetime
| Int32Array // int
| Uint8Array // bytes
| Uint32Array // Decimal
| Vector // arrays
| StructRow // interval
| Dictionary // categorical
| Struct // dict
| bigint // period
/** The type of the cell. */
export enum DataFrameCellType {
// Index cells
INDEX = 'index',
// Data cells
DATA = 'data',
}
/**
* Converts an Arrow vector to a list of strings.
*
* @param vector The Arrow vector to convert.
* @returns The list of strings.
*/
export function convertVectorToList(vector: Vector<any>): string[] {
const values: string[] = []
for (let i = 0; i < vector.length; i++) {
values.push(vector.get(i))
}
return values
}
/** Returns the timezone of the arrow type metadata. */
export function getTimezone(field: Field): string | undefined {
return field.type?.timezone ?? field.metadata?.get('timezone')
}
/**
* True if the arrow type is an integer type.
* For example: int8, int16, int32, int64, uint8, uint16, uint32, uint64, range
*/
export function isIntegerType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
// Period types are integers with an extra extension name
(ArrowDataType.isInt(field.type) && !isPeriodType(field))
|| isUnsignedIntegerType(field)
)
}
/** True if the arrow type is an unsigned integer type. */
export function isUnsignedIntegerType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
(ArrowDataType.isInt(field.type)
&& field.type.isSigned === false)
)
}
/**
* True if the arrow type is a float type.
* For example: float16, float32, float64, float96, float128
*/
export function isFloatType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
(ArrowDataType.isFloat(field.type))
?? false
)
}
/** True if the arrow type is a decimal type. */
export function isDecimalType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
ArrowDataType.isDecimal(field.type)
)
}
/** True if the arrow type is a numeric type. */
export function isNumericType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return isIntegerType(type) || isFloatType(type) || isDecimalType(type)
}
/** True if the arrow type is a boolean type. */
export function isBooleanType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isBool(type.type)
)
}
/** True if the arrow type is a duration type. */
export function isDurationType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDuration(type.type)
)
}
/** True if the arrow type is a period type. */
export function isPeriodType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
(ArrowDataType.isInt(type.type)
&& type.metadata.get('ARROW:extension:name') === 'period')
)
}
/** True if the arrow type is a datetime type. */
export function isDatetimeType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isTimestamp(type.type)
)
}
/** True if the arrow type is a date type. */
export function isDateType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDate(type.type)
)
}
/** True if the arrow type is a time type. */
export function isTimeType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isTime(type.type)
)
}
/** True if the arrow type is a categorical type. */
export function isCategoricalType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDictionary(type.type)
)
}
/** True if the arrow type is a list type. */
export function isListType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isList(type.type)
|| ArrowDataType.isFixedSizeList(type.type)
)
}
/** True if the arrow type is an object type. */
export function isObjectType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isStruct(type.type)
|| ArrowDataType.isMap(type.type)
)
}
/** True if the arrow type is a bytes type. */
export function isBytesType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isBinary(type.type)
|| ArrowDataType.isLargeBinary(type.type)
)
}
/** True if the arrow type is a string type. */
export function isStringType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isUtf8(type.type)
|| ArrowDataType.isLargeUtf8(type.type)
)
}
/** True if the arrow type is an empty type. */
export function isEmptyType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isNull(type.type)
)
}
/** True if the arrow type is a interval type. */
export function isIntervalType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
// ArrowDataType.isInterval checks for a different (unsupported) type and not related
// to the pandas interval extension type.
return (
((ArrowDataType.isStruct(type.type)
&& type.metadata.get('ARROW:extension:name') === 'interval'))
|| ArrowDataType.isInterval(type.type)
)
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"module": "ESNext",
"moduleResolution": "bundler",
"types": [
"vite/client"
],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts"
]
}
+1
View File
@@ -58,6 +58,7 @@
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@proj-airi/ccc": "workspace:^",
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
"@proj-airi/server-sdk": "workspace:^",
"@vueuse/motion": "^3.0.3",
"radix-vue": "^1.9.17",
+118 -767
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -7,6 +7,7 @@ packages:
- '!**/dist/**'
catalog:
'@proj-airi/drizzle-duckdb-wasm': ^0.4.20
'@xsai/shared': &xsai ^0.2.0-beta.3
'@xsai-ext/providers-cloud': *xsai
'@xsai-ext/providers-local': *xsai