diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..276ba34
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+EXPO_PUBLIC_SIGNING_SECRET=
diff --git a/CLAUDE.md b/CLAUDE.md
index 65e2897..0a9985f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -44,22 +44,30 @@ src/app/
entry/
create.tsx # Create entry /entry/create?journalId=...&journalName=...
[id].tsx # Entry detail /entry/[id]?journalName=...
- explore.tsx # Report tab
+ days/
+ _layout.tsx # Stack — scoped to days tab
+ index.tsx # Daily reflection list
+ settings.tsx # AI reflection settings (model, time, enabled)
+ search/
+ _layout.tsx # Stack — scoped to search tab
+ index.tsx # Journal entry search
```
**Navigation flow:** Journal list → `[id]` (entry list) → `entry/[id]` (entry detail)
-**Key rule:** `NativeTabs` is the root navigator; `Stack` lives inside `(journal)` group. This keeps the tab bar visible when pushing screens.
+**Key rule:** `NativeTabs` is the root navigator; `Stack` lives inside each tab group (`(journal)`, `days`, `search`). This keeps the tab bar visible when pushing screens.
### Database Layer
Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`:
-| File | Tables |
-| ------------- | ---------------------------------------- |
-| `journals.ts` | `journals` |
-| `fields.ts` | `fields` (field definitions per journal) |
-| `entries.ts` | `entries`, `entry_values` |
+| File | Tables |
+| ---------------- | ------------------------------------------------- |
+| `journals.ts` | `journals` |
+| `fields.ts` | `fields` (field definitions per journal) |
+| `entries.ts` | `entries`, `entry_values` |
+| `reflections.ts` | `reflections` (one AI-generated reflection/day) |
+| `settings.ts` | `settings` (KVS — key/value pairs for app config) |
```mermaid
erDiagram
@@ -95,6 +103,22 @@ erDiagram
TEXT value
}
+ reflections {
+ TEXT id PK
+ INTEGER date "unique, midnight timestamp"
+ TEXT title
+ TEXT firstCategory
+ TEXT firstContent
+ TEXT secondCategory
+ TEXT secondContent
+ INTEGER createdAt
+ }
+
+ settings {
+ TEXT key PK
+ TEXT value
+ }
+
journals ||--o{ fields : "has"
journals ||--o{ entries : "has"
entries ||--o{ entry_values : "has"
@@ -160,6 +184,24 @@ All field values are stored as `text | null` in `entry_values.value`. Serializat
- Remaining field values joined with spaces = preview (also used for search)
- Entries grouped by month for the list view (`groupByMonth`)
+### AI Reflection Pipeline
+
+On-device LLM generates daily reflections from journal entries:
+
+1. **Entry collection** — `src/db/queries/entries.ts` fetches all entries for a given day (`DailyEntryObj[]`)
+2. **Text conversion** — `src/utils/days/reflection/get-reflection.ts` formats entries into structured text (journal name + field labels/values)
+3. **LLM inference** — `@react-native-ai/llama` downloads a GGUF model, Vercel `ai` package's `generateText()` runs inference with a system prompt
+4. **Parsing** — JSON output is extracted via regex and validated with `reflectionSchema` (Zod)
+5. **Storage** — Result saved to `reflections` table (one per day, keyed by midnight timestamp)
+
+**Supported models** (defined in `src/constants/ai-models.ts`): Gemma 3 4B, Qwen 3 4B, Phi-4 Mini — all Q4_K_M quantization from HuggingFace.
+
+**Settings** are stored in the `settings` KVS table, managed by `src/hooks/settings/use-ai-reflection-settings.ts`:
+
+- `aiReflectionEnabled` — on/off toggle
+- `aiReflectionModelId` — selected GGUF model
+- `aiReflectionTime` — time of day to auto-generate (via `use-auto-reflection.ts`)
+
### Keyboard Dismiss Rule
Always call `Keyboard.dismiss()` **before** any `async` save operation that triggers navigation. Skipping this causes a `RemoteTextInput` session crash on iOS when the keyboard is mid-input as the screen unmounts.
@@ -176,6 +218,8 @@ Always call `Keyboard.dismiss()` **before** any `async` save operation that trig
| `expo-sqlite` + `drizzle-orm` | Local SQLite persistence |
| `expo-crypto` | `Crypto.randomUUID()` for ID generation at the app layer |
| `PlatformColor` | Adaptive system colors: `"label"`, `"systemBackground"`, `"systemIndigo"` |
+| `@react-native-ai/llama` | On-device GGUF model download + inference via `downloadModel()` and `llama.languageModel()` |
+| `ai` (Vercel AI SDK) | `generateText()` with structured prompts — used with llama model provider |
### SwiftUI Component Rules
diff --git a/drizzle/0001_loud_sally_floyd.sql b/drizzle/0001_loud_sally_floyd.sql
new file mode 100644
index 0000000..3e34b18
--- /dev/null
+++ b/drizzle/0001_loud_sally_floyd.sql
@@ -0,0 +1,4 @@
+CREATE TABLE `settings` (
+ `key` text PRIMARY KEY NOT NULL,
+ `value` text
+);
diff --git a/drizzle/0002_tiresome_brother_voodoo.sql b/drizzle/0002_tiresome_brother_voodoo.sql
new file mode 100644
index 0000000..05b5943
--- /dev/null
+++ b/drizzle/0002_tiresome_brother_voodoo.sql
@@ -0,0 +1,12 @@
+CREATE TABLE `reflections` (
+ `id` text PRIMARY KEY NOT NULL,
+ `date` integer NOT NULL,
+ `title` text NOT NULL,
+ `firstCategory` text NOT NULL,
+ `firstContent` text NOT NULL,
+ `secondCategory` text NOT NULL,
+ `secondContent` text NOT NULL,
+ `createdAt` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `reflections_date_unique` ON `reflections` (`date`);
\ No newline at end of file
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..14294ec
--- /dev/null
+++ b/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,268 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "c7f00b54-9b8b-464c-a516-f825371e613b",
+ "prevId": "3d118337-e09e-4635-9ca5-d9d5a5c94bf2",
+ "tables": {
+ "entries": {
+ "name": "entries",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "journalId": {
+ "name": "journalId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bookmark": {
+ "name": "bookmark",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "entries_journalId_journals_id_fk": {
+ "name": "entries_journalId_journals_id_fk",
+ "tableFrom": "entries",
+ "tableTo": "journals",
+ "columnsFrom": ["journalId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "entry_values": {
+ "name": "entry_values",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entryId": {
+ "name": "entryId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "fieldId": {
+ "name": "fieldId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "entry_field_unique": {
+ "name": "entry_field_unique",
+ "columns": ["entryId", "fieldId"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "entry_values_entryId_entries_id_fk": {
+ "name": "entry_values_entryId_entries_id_fk",
+ "tableFrom": "entry_values",
+ "tableTo": "entries",
+ "columnsFrom": ["entryId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "entry_values_fieldId_fields_id_fk": {
+ "name": "entry_values_fieldId_fields_id_fk",
+ "tableFrom": "entry_values",
+ "tableTo": "fields",
+ "columnsFrom": ["fieldId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "fields": {
+ "name": "fields",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "journalId": {
+ "name": "journalId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sortOrder": {
+ "name": "sortOrder",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "fields_journalId_journals_id_fk": {
+ "name": "fields_journalId_journals_id_fk",
+ "tableFrom": "fields",
+ "tableTo": "journals",
+ "columnsFrom": ["journalId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "journals": {
+ "name": "journals",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "settings": {
+ "name": "settings",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..195a65d
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,340 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "8a42d699-6379-4a82-bbbe-cbc89fe1ee99",
+ "prevId": "c7f00b54-9b8b-464c-a516-f825371e613b",
+ "tables": {
+ "entries": {
+ "name": "entries",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "journalId": {
+ "name": "journalId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bookmark": {
+ "name": "bookmark",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "entries_journalId_journals_id_fk": {
+ "name": "entries_journalId_journals_id_fk",
+ "tableFrom": "entries",
+ "tableTo": "journals",
+ "columnsFrom": ["journalId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "entry_values": {
+ "name": "entry_values",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "entryId": {
+ "name": "entryId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "fieldId": {
+ "name": "fieldId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "entry_field_unique": {
+ "name": "entry_field_unique",
+ "columns": ["entryId", "fieldId"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "entry_values_entryId_entries_id_fk": {
+ "name": "entry_values_entryId_entries_id_fk",
+ "tableFrom": "entry_values",
+ "tableTo": "entries",
+ "columnsFrom": ["entryId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "entry_values_fieldId_fields_id_fk": {
+ "name": "entry_values_fieldId_fields_id_fk",
+ "tableFrom": "entry_values",
+ "tableTo": "fields",
+ "columnsFrom": ["fieldId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "fields": {
+ "name": "fields",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "journalId": {
+ "name": "journalId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sortOrder": {
+ "name": "sortOrder",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "fields_journalId_journals_id_fk": {
+ "name": "fields_journalId_journals_id_fk",
+ "tableFrom": "fields",
+ "tableTo": "journals",
+ "columnsFrom": ["journalId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "journals": {
+ "name": "journals",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "settings": {
+ "name": "settings",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "reflections": {
+ "name": "reflections",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "date": {
+ "name": "date",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "firstCategory": {
+ "name": "firstCategory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "firstContent": {
+ "name": "firstContent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "secondCategory": {
+ "name": "secondCategory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "secondContent": {
+ "name": "secondContent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "reflections_date_unique": {
+ "name": "reflections_date_unique",
+ "columns": ["date"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index bec3a13..091fc06 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -8,6 +8,20 @@
"when": 1777997392852,
"tag": "0000_young_bulldozer",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1786688489216,
+ "tag": "0001_loud_sally_floyd",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "6",
+ "when": 1786863241243,
+ "tag": "0002_tiresome_brother_voodoo",
+ "breakpoints": true
}
]
}
diff --git a/drizzle/migrations.js b/drizzle/migrations.js
index 0fc3525..1cc3864 100644
--- a/drizzle/migrations.js
+++ b/drizzle/migrations.js
@@ -1,11 +1,15 @@
// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo
import m0000 from "./0000_young_bulldozer.sql";
+import m0001 from "./0001_loud_sally_floyd.sql";
+import m0002 from "./0002_tiresome_brother_voodoo.sql";
import journal from "./meta/_journal.json";
export default {
journal,
migrations: {
m0000,
+ m0001,
+ m0002,
},
};
diff --git a/package.json b/package.json
index 54b42e8..9a75a07 100644
--- a/package.json
+++ b/package.json
@@ -16,9 +16,13 @@
},
"dependencies": {
"@expo/ui": "~55.0.17",
+ "@react-native-ai/llama": "^0.12.0",
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33",
+ "@stardazed/streams-text-encoding": "^1.0.2",
+ "@ungap/structured-clone": "^1.3.3",
+ "ai": "^7.0.58",
"babel-plugin-inline-import": "^3.0.0",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
@@ -44,6 +48,7 @@
"expo-symbols": "~55.0.7",
"expo-system-ui": "~55.0.16",
"expo-web-browser": "~55.0.14",
+ "llama.rn": "0.12.0-rc.9",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e1b27a4..1c2f335 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,9 @@ importers:
'@expo/ui':
specifier: ~55.0.17
version: 55.0.17(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-native-ai/llama':
+ specifier: ^0.12.0
+ version: 0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
'@react-navigation/bottom-tabs':
specifier: ^7.15.5
version: 7.15.11(@react-navigation/native@7.2.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -30,6 +33,15 @@ importers:
'@react-navigation/native':
specifier: ^7.1.33
version: 7.2.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@stardazed/streams-text-encoding':
+ specifier: ^1.0.2
+ version: 1.0.2
+ '@ungap/structured-clone':
+ specifier: ^1.3.3
+ version: 1.3.3
+ ai:
+ specifier: ^7.0.58
+ version: 7.0.58(zod@4.4.3)
babel-plugin-inline-import:
specifier: ^3.0.0
version: 3.0.0
@@ -105,6 +117,9 @@ importers:
expo-web-browser:
specifier: ~55.0.14
version: 55.0.14(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
+ llama.rn:
+ specifier: 0.12.0-rc.9
+ version: 0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react:
specifier: 19.2.0
version: 19.2.0
@@ -154,6 +169,32 @@ importers:
packages:
+ '@ai-sdk/gateway@4.0.46':
+ resolution: {integrity: sha512-LIAO6kAG8fpXQb9L0iwPk1FIbXftvqnyC56v5NEAzeWTeL8fUsy/Hx86VPBTWEDFdwbVprjWifJOAqS6AOj3mA==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
+ '@ai-sdk/provider-utils@4.0.42':
+ resolution: {integrity: sha512-Q07lZsq4ir+xwAGVfSbY2WNGLrbfWINFaL2I/vTBFrkuXeP7VZh+UtQgLOKZS6Z/6flNFW22jDYdbINvwTV/PA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
+ '@ai-sdk/provider-utils@5.0.25':
+ resolution: {integrity: sha512-xscPPHCSjCHWrdhai25sbHCJeKNLW/3D1uSpUZa4cEtTKXA8OnPQ3+Rfu1SmM5Ea/Mf8Dfn3cllw9zeMzo/zFA==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
+ '@ai-sdk/provider@3.0.14':
+ resolution: {integrity: sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==}
+ engines: {node: '>=18'}
+
+ '@ai-sdk/provider@4.0.7':
+ resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==}
+ engines: {node: '>=22'}
+
'@babel/code-frame@7.10.4':
resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}
@@ -1383,6 +1424,10 @@ packages:
resolution: {integrity: sha512-wC562eD3gS6vO2tWHToFhlFnmHKfKHgF1oyvojeSkLK/ZYop1bMU+7cOMiF9Sq70CzcsLy/EMRy/uRc76QmNRw==}
hasBin: true
+ '@fastify/busboy@2.1.1':
+ resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
+ engines: {node: '>=14'}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -1991,6 +2036,12 @@ packages:
'@types/react':
optional: true
+ '@react-native-ai/llama@0.12.0':
+ resolution: {integrity: sha512-v/zhk9SEAk7P4/iELHMkAfIEEIn1TaMdIwfaFv41Yu3wm3sRbws7qRufTeq3nFBnSLj3MtVMZy4Vew7Fy5uyaA==}
+ peerDependencies:
+ llama.rn: ^0.10.1
+ react-native: '>=0.76.0'
+
'@react-native/assets-registry@0.83.6':
resolution: {integrity: sha512-iljb4ue1yWJ3EhySz7EjV6CzSVrI2uNtR8BI2jzP5+QS5E4Cl3fdIJRmVwDEx1pu8uE97PGEusGRHnoaZ9Q3jg==}
engines: {node: '>= 20.19.4'}
@@ -2212,6 +2263,9 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@stardazed/streams-text-encoding@1.0.2':
+ resolution: {integrity: sha512-f2Z15BId3t44a/u21yYSGXFAkCyKocmAyduoAy7swnZ4xIfbaZlOWsgly/jDNNOuj6hYQN72UaBRe3Z/tOHfqg==}
+
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@@ -2346,6 +2400,10 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ deprecated: Potential CWE-502 - Update to 1.3.1 or higher
+
+ '@ungap/structured-clone@1.3.3':
+ resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@@ -2450,6 +2508,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@vercel/oidc@3.2.0':
+ resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
+ engines: {node: '>= 20'}
+
'@vitest/browser-preview@4.1.10':
resolution: {integrity: sha512-14MJrL59ZFkqXLjwfSk6RzTDy5Czf9UG4+8q8L6Gxjs2aPjEce/cVNYV14bXAc2BvMjUNu904+ZEZA1Xc1wtvQ==}
peerDependencies:
@@ -2692,6 +2754,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@workflow/serde@4.1.0':
+ resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==}
+
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
@@ -2851,6 +2916,12 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ ai@7.0.58:
+ resolution: {integrity: sha512-GfgO90CQQ0yYuoxJAUOeQ6tviyYw1BUIDygSZ1q3Ce6kSc93tYmB5eltKY/NxC0YOouAax7JvDqVnYxvIAr04Q==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
ajv@6.15.0:
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
@@ -3031,6 +3102,9 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
+ base-64@1.0.0:
+ resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
+
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -3650,6 +3724,10 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
+ eventsource-parser@3.1.0:
+ resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
+ engines: {node: '>=18.0.0'}
+
expo-asset@55.0.16:
resolution: {integrity: sha512-5IJyfJtYqvKGg04NKGQWiCIoK/fULDL9m15mXPPyfabD1jsToVj2hnWmo1r2SWNNmMwtQxi6jTpcGwVo2nLDxg==}
peerDependencies:
@@ -4336,6 +4414,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ json-schema@0.4.0:
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -4522,6 +4603,14 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
+ llama.rn@0.12.0-rc.9:
+ resolution: {integrity: sha512-6qmo0sOGs7mk7stAGPtQ13R002yV2sYWR5z0YmUNlBUWVeOVeSXfsG2CTAfbyEr6fjXhFNgZVPYEt0Hd7Yd27A==}
+ engines: {node: '>= 16.0.0'}
+ hasBin: true
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -5059,6 +5148,12 @@ packages:
react-is@19.2.5:
resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==}
+ react-native-blob-util@0.24.10:
+ resolution: {integrity: sha512-4yazgoCstXgt2/CRURtowGP3furAzbWdfMQ4jyUPDo5xtLx7KE2F/xMz3oU3shLwfL4id0iEKsBywoxvJSHPzQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
react-native-gesture-handler@2.30.1:
resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
peerDependencies:
@@ -5618,6 +5713,14 @@ packages:
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
+ undici@5.29.0:
+ resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
+ engines: {node: '>=14.0'}
+
+ undici@7.29.0:
+ resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
+ engines: {node: '>=20.18.1'}
+
unicode-canonical-property-names-ecmascript@2.0.1:
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
engines: {node: '>=4'}
@@ -5900,6 +6003,38 @@ packages:
snapshots:
+ '@ai-sdk/gateway@4.0.46(zod@4.4.3)':
+ dependencies:
+ '@ai-sdk/provider': 4.0.7
+ '@ai-sdk/provider-utils': 5.0.25(zod@4.4.3)
+ '@vercel/oidc': 3.2.0
+ zod: 4.4.3
+
+ '@ai-sdk/provider-utils@4.0.42(zod@4.4.3)':
+ dependencies:
+ '@ai-sdk/provider': 3.0.14
+ '@standard-schema/spec': 1.1.0
+ eventsource-parser: 3.1.0
+ undici: 5.29.0
+ zod: 4.4.3
+
+ '@ai-sdk/provider-utils@5.0.25(zod@4.4.3)':
+ dependencies:
+ '@ai-sdk/provider': 4.0.7
+ '@standard-schema/spec': 1.1.0
+ '@workflow/serde': 4.1.0
+ eventsource-parser: 3.1.0
+ undici: 7.29.0
+ zod: 4.4.3
+
+ '@ai-sdk/provider@3.0.14':
+ dependencies:
+ json-schema: 0.4.0
+
+ '@ai-sdk/provider@4.0.7':
+ dependencies:
+ json-schema: 0.4.0
+
'@babel/code-frame@7.10.4':
dependencies:
'@babel/highlight': 7.25.9
@@ -7241,6 +7376,8 @@ snapshots:
chalk: 4.1.2
js-yaml: 4.1.1
+ '@fastify/busboy@2.1.1': {}
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -7693,6 +7830,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@react-native-ai/llama@0.12.0(llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@ai-sdk/provider': 3.0.14
+ '@ai-sdk/provider-utils': 4.0.42(zod@4.4.3)
+ llama.rn: 0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
+ react-native-blob-util: 0.24.10(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ zod: 4.4.3
+ transitivePeerDependencies:
+ - react
+
'@react-native/assets-registry@0.83.6': {}
'@react-native/babel-plugin-codegen@0.83.6(@babel/core@7.29.0)':
@@ -7941,6 +8089,8 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@stardazed/streams-text-encoding@1.0.2': {}
+
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0
@@ -8122,6 +8272,8 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@ungap/structured-clone@1.3.3': {}
+
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
optional: true
@@ -8181,6 +8333,8 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
+ '@vercel/oidc@3.2.0': {}
+
'@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-test@0.1.24(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(typescript@5.9.3)(vite@8.2.0(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3))(vite@8.2.0(@types/node@25.6.0)(esbuild@0.27.7)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@testing-library/dom': 10.4.1
@@ -8347,6 +8501,8 @@ snapshots:
'@voidzero-dev/vite-plus-win32-x64-msvc@0.2.7':
optional: true
+ '@workflow/serde@4.1.0': {}
+
'@xmldom/xmldom@0.8.13': {}
'@xmldom/xmldom@0.9.10': {}
@@ -8441,6 +8597,13 @@ snapshots:
agent-base@7.1.4: {}
+ ai@7.0.58(zod@4.4.3):
+ dependencies:
+ '@ai-sdk/gateway': 4.0.46(zod@4.4.3)
+ '@ai-sdk/provider': 4.0.7
+ '@ai-sdk/provider-utils': 5.0.25(zod@4.4.3)
+ zod: 4.4.3
+
ajv@6.15.0:
dependencies:
fast-deep-equal: 3.1.3
@@ -8708,6 +8871,8 @@ snapshots:
balanced-match@4.0.4: {}
+ base-64@1.0.0: {}
+
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.24: {}
@@ -9419,6 +9584,8 @@ snapshots:
event-target-shim@5.0.1: {}
+ eventsource-parser@3.1.0: {}
+
expo-asset@55.0.16(expo@55.0.18)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
dependencies:
'@expo/image-utils': 0.8.13(typescript@5.9.3)
@@ -10196,6 +10363,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
+ json-schema@0.4.0: {}
+
json-stable-stringify-without-jsonify@1.0.1: {}
json5@1.0.2:
@@ -10331,6 +10500,11 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
+ llama.rn@0.12.0-rc.9(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
+
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -10963,6 +11137,13 @@ snapshots:
react-is@19.2.5: {}
+ react-native-blob-util@0.24.10(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ base-64: 1.0.0
+ glob: 13.0.6
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
+
react-native-gesture-handler@2.30.1(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
'@egjs/hammerjs': 2.0.17
@@ -11627,6 +11808,12 @@ snapshots:
undici-types@7.19.2: {}
+ undici@5.29.0:
+ dependencies:
+ '@fastify/busboy': 2.1.1
+
+ undici@7.29.0: {}
+
unicode-canonical-property-names-ecmascript@2.0.1: {}
unicode-match-property-ecmascript@2.0.0:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 386d7b1..b24ec0a 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,10 +1,11 @@
allowBuilds:
esbuild: true
+ llama.rn: true
unrs-resolver: true
catalog:
vite: npm:@voidzero-dev/vite-plus-core@latest
- vitest: npm:@voidzero-dev/vite-plus-test@latest
vite-plus: latest
+ vitest: npm:@voidzero-dev/vite-plus-test@latest
overrides:
vite: "catalog:"
vitest: "catalog:"
diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx
index dd8242d..eede295 100644
--- a/src/app/(journal)/create.tsx
+++ b/src/app/(journal)/create.tsx
@@ -1,14 +1,14 @@
import { useState } from "react";
-import { Alert, Keyboard, PlatformColor } from "react-native";
+import { Keyboard, PlatformColor } from "react-native";
import * as Crypto from "expo-crypto";
import { Stack, useRouter } from "expo-router";
-import { z } from "zod";
import { JournalCreateView } from "@/components/journal/journal-create-view";
+import { useJournalField } from "@/hooks/journal/use-journal-field";
import { importJournal } from "@/utils/days/import-journal";
+import { handleSaveError } from "@/utils/handle-save-error";
import { setCreatedJournalId } from "@/utils/journal/created-journal";
-import { useJournalField } from "@/utils/journal/use-journal-field";
/**
* ジャーナル作成
@@ -39,9 +39,7 @@ export default function JournalCreateScreen() {
setCreatedJournalId(id);
router.back();
} catch (error) {
- if (error instanceof z.ZodError) {
- Alert.alert("Validation Error", error.issues[0].message);
- }
+ handleSaveError(error);
}
};
diff --git a/src/app/(journal)/edit.tsx b/src/app/(journal)/edit.tsx
index 76b18f2..08d2e75 100644
--- a/src/app/(journal)/edit.tsx
+++ b/src/app/(journal)/edit.tsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { Alert as RNAlert, Keyboard, PlatformColor } from "react-native";
+import { Keyboard, PlatformColor } from "react-native";
import { Alert, Button, Host, Text } from "@expo/ui/swift-ui";
import { useLiveQuery } from "drizzle-orm/expo-sqlite";
@@ -9,9 +9,10 @@ import { z } from "zod";
import { JournalCreateView } from "@/components/journal/journal-create-view";
import { deleteJournal, getJournalDetailQuery, JournalDetail } from "@/db/queries/journals";
import { FieldObj } from "@/db/schemas";
+import { FieldDraftObj, useJournalField } from "@/hooks/journal/use-journal-field";
+import { useValidatedParams } from "@/hooks/use-validated-params";
import { exportJournal } from "@/utils/days/export-journal";
-import { FieldDraftObj, useJournalField } from "@/utils/journal/use-journal-field";
-import { useValidatedParams } from "@/utils/params";
+import { handleSaveError } from "@/utils/handle-save-error";
type FormProps = {
journal: JournalDetail;
@@ -54,9 +55,7 @@ function JournalEditForm({ journal }: FormProps) {
await updateJournal(journal.id);
router.back();
} catch (error) {
- if (error instanceof z.ZodError) {
- RNAlert.alert("Validation Error", error.issues[0].message);
- }
+ handleSaveError(error);
}
};
@@ -155,7 +154,9 @@ function JournalEditForm({ journal }: FormProps) {
* ジャーナル編集
*/
export default function JournalEditScreen() {
- const { journalId } = useValidatedParams(z.object({ journalId: z.string() }));
+ const schema = z.object({ journalId: z.string() });
+ const { journalId } = useValidatedParams(schema);
+
const { data: journal } = useLiveQuery(getJournalDetailQuery(journalId), [journalId]);
return <>{journal && }>;
diff --git a/src/app/(journal)/entry/[id].tsx b/src/app/(journal)/entry/[id].tsx
index 7e189c1..02a09a4 100644
--- a/src/app/(journal)/entry/[id].tsx
+++ b/src/app/(journal)/entry/[id].tsx
@@ -1,13 +1,14 @@
import { useState } from "react";
-import { Alert, PlatformColor } from "react-native";
+import { PlatformColor } from "react-native";
+import { useLiveQuery } from "drizzle-orm/expo-sqlite";
import { Stack, useRouter } from "expo-router";
import { z } from "zod";
-import { EntryCreateView } from "@/components/entry/entry-create-view";
import { EntryDetailView } from "@/components/entry/entry-detail";
-import { useEntryDetail } from "@/utils/entry/use-entry-detail";
-import { useValidatedParams } from "@/utils/params";
+import { EntryEditView } from "@/components/entry/entry-edit-view";
+import { bookmarkEntry, deleteEntry, getEntryDetailQuery } from "@/db/queries/entries";
+import { useValidatedParams } from "@/hooks/use-validated-params";
/**
* エントリー詳細
@@ -15,106 +16,87 @@ import { useValidatedParams } from "@/utils/params";
export default function EntryDetailScreen() {
const router = useRouter();
- const {
- id: entryId,
- journalName,
- edit,
- } = useValidatedParams(
- z.object({
- id: z.string(),
- journalName: z.string(),
- edit: z
- .string()
- .optional()
- .transform((v) => v === "true"),
- }),
- );
+ const shema = z.object({
+ id: z.string(),
+ journalName: z.string(),
+ edit: z
+ .string()
+ .optional()
+ .transform((v) => v === "true"),
+ });
+ const { id: entryId, journalName, edit } = useValidatedParams(shema);
const [editMode, setEditMode] = useState(edit);
-
- const { entry, valuesRef, setValue, save, bookmark, remove } = useEntryDetail(entryId);
-
- const handleSave = async () => {
- try {
- await save();
- setEditMode(false);
- } catch (error) {
- if (error instanceof z.ZodError) {
- Alert.alert("Validation Error", error.issues[0].message);
- }
- }
- };
+ const { data: entry } = useLiveQuery(getEntryDetailQuery(entryId), [entryId]);
const handleDelete = async () => {
- await remove();
+ await deleteEntry(entryId);
router.back();
};
+ const handleBookmark = async () => {
+ if (entry) await bookmarkEntry(entry.id, !entry.bookmark);
+ };
+
return (
<>
- editMode
- ? [
- {
- type: "button",
- label: "Cancel",
- onPress: () => setEditMode(false),
- },
- {
- type: "button",
- label: "Save",
- icon: { type: "sfSymbol", name: "checkmark" },
- tintColor: PlatformColor("systemIndigo"),
- variant: "prominent",
- onPress: handleSave,
- },
- ]
- : [
- {
- type: "menu",
- label: "Options",
- icon: { type: "sfSymbol", name: "ellipsis" },
- menu: {
- items: [
- {
- type: "action",
- icon: {
- type: "sfSymbol",
- name: entry?.bookmark ? "bookmark.slash" : "bookmark",
- },
- label: entry?.bookmark ? "Unbookmark" : "Bookmark",
- onPress: bookmark,
- },
- {
- type: "action",
- label: "Delete Entry",
- icon: { type: "sfSymbol", name: "trash" },
- destructive: true,
- onPress: handleDelete,
+ unstable_headerRightItems: editMode
+ ? undefined
+ : () => [
+ {
+ type: "menu",
+ label: "Options",
+ icon: { type: "sfSymbol", name: "ellipsis" },
+ menu: {
+ items: [
+ {
+ type: "action",
+ icon: {
+ type: "sfSymbol",
+ name: "square.and.arrow.up",
},
- ],
- },
+ label: "Export Entry",
+ onPress: () => console.log("Export Entry"),
+ },
+ {
+ type: "action",
+ label: "Delete Entry",
+ icon: { type: "sfSymbol", name: "trash" },
+ destructive: true,
+ onPress: handleDelete,
+ },
+ ],
},
- {
- type: "button",
- label: "Edit",
- onPress: () => setEditMode(true),
+ },
+ {
+ type: "button",
+ icon: {
+ type: "sfSymbol",
+ name: entry?.bookmark ? "bookmark.fill" : "bookmark",
},
- ],
+ label: entry?.bookmark ? "Unbookmark" : "Bookmark",
+ tintColor: entry?.bookmark ? PlatformColor("systemIndigo") : undefined,
+ onPress: handleBookmark,
+ },
+ {
+ type: "button",
+ label: "Edit",
+ onPress: () => setEditMode(true),
+ },
+ ],
}}
/>
{entry &&
(editMode ? (
- setEditMode(false)}
+ onCancel={() => setEditMode(false)}
/>
) : (
diff --git a/src/app/(journal)/entry/create.tsx b/src/app/(journal)/entry/create.tsx
index c090ee5..6f34b3e 100644
--- a/src/app/(journal)/entry/create.tsx
+++ b/src/app/(journal)/entry/create.tsx
@@ -1,4 +1,4 @@
-import { Alert, Keyboard, PlatformColor } from "react-native";
+import { Keyboard, PlatformColor } from "react-native";
import { useLiveQuery } from "drizzle-orm/expo-sqlite";
import { Stack, useRouter } from "expo-router";
@@ -6,8 +6,9 @@ import { z } from "zod";
import { EntryCreateView } from "@/components/entry/entry-create-view";
import { getFieldsQuery } from "@/db/queries/fields";
-import { useEntry } from "@/utils/entry/use-entry";
-import { useValidatedParams } from "@/utils/params";
+import { useEntry } from "@/hooks/entry/use-entry";
+import { useValidatedParams } from "@/hooks/use-validated-params";
+import { handleSaveError } from "@/utils/handle-save-error";
/**
* エントリー作成
@@ -15,9 +16,8 @@ import { useValidatedParams } from "@/utils/params";
export default function EntryCreateScreen() {
const router = useRouter();
- const { journalId, journalName } = useValidatedParams(
- z.object({ journalId: z.string(), journalName: z.string() }),
- );
+ const schema = z.object({ journalId: z.string(), journalName: z.string() });
+ const { journalId, journalName } = useValidatedParams(schema);
const { data: fields } = useLiveQuery(getFieldsQuery(journalId), [journalId]);
const { valuesRef, setValue, createEntry } = useEntry(fields);
@@ -29,9 +29,7 @@ export default function EntryCreateScreen() {
const { id: newEntryId } = await createEntry(journalId);
router.replace(`/(journal)/entry/${newEntryId}?journalName=${journalName}`);
} catch (error) {
- if (error instanceof z.ZodError) {
- Alert.alert("Validation Error", error.issues[0].message);
- }
+ handleSaveError(error);
}
};
diff --git a/src/app/(journal)/index.tsx b/src/app/(journal)/index.tsx
index ead0df1..ec4a7e7 100644
--- a/src/app/(journal)/index.tsx
+++ b/src/app/(journal)/index.tsx
@@ -64,7 +64,7 @@ export default function JournalScreen() {
type: "action" as const,
icon: {
type: "sfSymbol" as const,
- name: bookmarkOnly ? ("bookmark.fill" as const) : ("bookmark" as const),
+ name: bookmarkOnly ? ("bookmark" as const) : ("bookmark.fill" as const),
},
label: bookmarkOnly ? "Show All" : "Bookmarked Only",
onPress: () => setBookmarkOnly((prev) => !prev),
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 5d4f83c..f69d5eb 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -1,12 +1,19 @@
+import "@/polyfills";
import React from "react";
import AppTabs from "@/components/app-tabs";
import { DrizzleProvider } from "@/components/drizzle-provider";
+import { useAutoReflection } from "@/hooks/settings/use-auto-reflection";
+
+function AppContent() {
+ useAutoReflection();
+ return ;
+}
export default function RootLayout() {
return (
-
+
);
}
diff --git a/src/app/days/_layout.tsx b/src/app/days/_layout.tsx
index dd76c45..0efa613 100644
--- a/src/app/days/_layout.tsx
+++ b/src/app/days/_layout.tsx
@@ -17,7 +17,17 @@ export default function DaysLayout() {
},
headerBackButtonDisplayMode: "minimal",
}}
- />
+ >
+
+
+
);
}
diff --git a/src/app/days/index.tsx b/src/app/days/index.tsx
index 4b6880a..47ae70f 100644
--- a/src/app/days/index.tsx
+++ b/src/app/days/index.tsx
@@ -18,10 +18,10 @@ import Animated, {
import { BottomSheet, DatePicker, Host } from "@expo/ui/swift-ui";
import { datePickerStyle, tint } from "@expo/ui/swift-ui/modifiers";
import { GlassView } from "expo-glass-effect";
-import { Stack } from "expo-router";
+import { Stack, useRouter } from "expo-router";
import { DaysView } from "@/components/days/days-view";
-import { addDays, formatDateDays } from "@/utils/date";
+import { addDays, formatDateDays, startOfDay } from "@/utils/date";
const SWIPE_THRESHOLD = 50;
@@ -30,12 +30,9 @@ const SWIPE_THRESHOLD = 50;
*/
export default function DaysScreen() {
const { width: screenWidth } = useWindowDimensions();
- const [selectedDate, setSelectedDate] = useState(() => {
- const d = new Date();
- d.setHours(0, 0, 0, 0);
- return d;
- });
+ const [selectedDate, setSelectedDate] = useState(() => startOfDay());
const [showCalendar, setShowCalendar] = useState(false);
+ const router = useRouter();
const glassWidth = useSharedValue(200);
@@ -44,8 +41,7 @@ export default function DaysScreen() {
const prevDate = addDays(selectedDate, -1);
const nextDate = addDays(selectedDate, 1);
- const today = new Date();
- today.setHours(0, 0, 0, 0);
+ const today = startOfDay();
const canGoNext = nextDate <= today;
@@ -141,7 +137,7 @@ export default function DaysScreen() {
type: "button",
label: "Save",
icon: { type: "sfSymbol", name: "gearshape" },
- onPress: () => console.log("gear"),
+ onPress: () => router.navigate("/days/settings"),
},
],
}}
diff --git a/src/app/days/settings.tsx b/src/app/days/settings.tsx
new file mode 100644
index 0000000..fca7b6e
--- /dev/null
+++ b/src/app/days/settings.tsx
@@ -0,0 +1,26 @@
+import { PlatformColor } from "react-native";
+
+import { Host, List } from "@expo/ui/swift-ui";
+import { frame } from "@expo/ui/swift-ui/modifiers";
+
+import { Application } from "@/components/settings/application";
+import { EntrySettings } from "@/components/settings/entry";
+import { Support } from "@/components/settings/support";
+
+/**
+ * Nicky 設定画面
+ */
+export default function SettingsScreen() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/days/days-card.tsx b/src/components/days/days-card.tsx
index b2fff1b..0c58e03 100644
--- a/src/components/days/days-card.tsx
+++ b/src/components/days/days-card.tsx
@@ -5,8 +5,8 @@ import { font, foregroundStyle, listRowSeparator, opacity } from "@expo/ui/swift
import { EntryDetailObj } from "@/db/queries/entries";
import { JournalObj } from "@/db/schemas";
+import { deserializeValue } from "@/hooks/entry/use-entry";
import { formatTime } from "@/utils/date";
-import { deserializeValue } from "@/utils/entry/use-entry";
import { EntryFieldItem } from "../entry/entry-field-item";
diff --git a/src/components/days/days-llm-fb.tsx b/src/components/days/days-llm-fb.tsx
deleted file mode 100644
index 5168098..0000000
--- a/src/components/days/days-llm-fb.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { PlatformColor } from "react-native";
-
-import { HStack, Image, Section, Text } from "@expo/ui/swift-ui";
-import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers";
-
-import { FieldWrapper } from "../field/field-wrapper";
-
-/**
- * LLM によるフィードバックを表示する
- */
-export function DaysLLMFB() {
- return (
-
-
-
-
- {`Today's reflection`}
-
-
-
- }
- >
-
- hogehoge
-
-
- fugafuga
-
-
- );
-}
diff --git a/src/components/days/days-llm-reflection.tsx b/src/components/days/days-llm-reflection.tsx
new file mode 100644
index 0000000..90d5699
--- /dev/null
+++ b/src/components/days/days-llm-reflection.tsx
@@ -0,0 +1,52 @@
+import { PlatformColor } from "react-native";
+
+import { HStack, Image, Section, Text } from "@expo/ui/swift-ui";
+import { font, foregroundStyle } from "@expo/ui/swift-ui/modifiers";
+import { useLiveQuery } from "drizzle-orm/expo-sqlite";
+
+import { getReflectionByDateQuery } from "@/db/queries/reflections";
+import { useAIReflectionSettings } from "@/hooks/settings/use-ai-reflection-settings";
+
+import { FieldWrapper } from "../field/field-wrapper";
+
+type Props = {
+ /** 表示対象の日付 */
+ date: Date;
+};
+
+/**
+ * AI Reflection を表示する
+ */
+export function DaysLLMReflection({ date }: Props) {
+ const { aiReflectionEnabled } = useAIReflectionSettings();
+ const { data: reflection } = useLiveQuery(getReflectionByDateQuery(date), [date.getTime()]);
+
+ if (!aiReflectionEnabled || !reflection) return null;
+
+ return (
+
+
+
+
+ {reflection.title}
+
+
+
+ }
+ >
+
+ {reflection.firstContent}
+
+
+ {reflection.secondContent}
+
+
+ );
+}
diff --git a/src/components/days/days-view.tsx b/src/components/days/days-view.tsx
index 81cab05..f46d0ba 100644
--- a/src/components/days/days-view.tsx
+++ b/src/components/days/days-view.tsx
@@ -8,9 +8,12 @@ import { useLiveQuery } from "drizzle-orm/expo-sqlite";
import { getEntriesByDateQuery } from "@/db/queries/entries";
import { DaysCard } from "./days-card";
-import { DaysLLMFB } from "./days-llm-fb";
+import { DaysLLMReflection } from "./days-llm-reflection";
type Props = {
+ /**
+ * 日付
+ */
date: Date;
};
@@ -56,7 +59,7 @@ export function DaysView({ date }: Props) {
animation(Animation.default, expandedIds.size),
]}
>
-
+
{entries.map((entry, index) => (
-
- {formatDate(entry.createdAt)}
-
- {entry.bookmark && (
-
- )}
-
- }
- >
+ {formatDate(entry.createdAt)}}>
{sorted.map((v) => (
void;
+ onCancel: () => void;
+}) {
+ const { fields, initialValues } = buildEntryFormData(entry);
+ const { valuesRef, setValue, updateEntry } = useEntry(fields, initialValues);
+
+ const handleSave = async () => {
+ try {
+ Keyboard.dismiss();
+ await updateEntry(entry.id);
+ onSave();
+ } catch (error) {
+ handleSaveError(error);
+ }
+ };
+
+ return (
+ <>
+ [
+ {
+ type: "button",
+ label: "Cancel",
+ onPress: onCancel,
+ },
+ {
+ type: "button",
+ label: "Save",
+ icon: { type: "sfSymbol", name: "checkmark" },
+ tintColor: PlatformColor("systemIndigo"),
+ variant: "prominent",
+ onPress: handleSave,
+ },
+ ],
+ }}
+ />
+
+ >
+ );
+}
diff --git a/src/components/entry/entry-field-item.tsx b/src/components/entry/entry-field-item.tsx
index c29183e..80cfe9e 100644
--- a/src/components/entry/entry-field-item.tsx
+++ b/src/components/entry/entry-field-item.tsx
@@ -7,7 +7,7 @@ import { EntryMedia } from "@/components/field/entry-media";
import { EntryText } from "@/components/field/entry-text";
import { EntryTime } from "@/components/field/entry-time";
import type { FieldObj } from "@/db/schemas";
-import { type FieldValue } from "@/utils/entry/use-entry";
+import { type FieldValue } from "@/hooks/entry/use-entry";
type Props = {
/** フィールド定義 */
diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx
index 76be9d3..d823974 100644
--- a/src/components/entry/entry-list-view.tsx
+++ b/src/components/entry/entry-list-view.tsx
@@ -9,7 +9,7 @@ import { SymbolView } from "expo-symbols";
import { JournalChipList } from "@/components/journal/journal-chip";
import { deleteEntry } from "@/db/queries/entries";
import { type JournalWithCountObj } from "@/db/queries/journals";
-import { useEntryList } from "@/utils/entry/use-entry-list";
+import { useEntryList } from "@/hooks/entry/use-entry-list";
import { EntryRow } from "./entry-row";
diff --git a/src/components/entry/entry-row.tsx b/src/components/entry/entry-row.tsx
index ee6ed5d..86ecb5b 100644
--- a/src/components/entry/entry-row.tsx
+++ b/src/components/entry/entry-row.tsx
@@ -55,7 +55,7 @@ export function EntryRow({ journalName, entry }: Props) {
/>