mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 08:52:42 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
-607
@@ -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>
|
||||
-339
@@ -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>
|
||||
-269
@@ -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>
|
||||
-487
@@ -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' }])
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from './common'
|
||||
export * from './duckdb'
|
||||
export * from './format'
|
||||
export * from './storage'
|
||||
export * from './types'
|
||||
@@ -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
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+118
-767
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user